#9 儲存資料的方法 UserDefaults

我們來試做一個簡單的App遊戲, 當使用者剛創立角色時, 人物等級為1, 當按下”打怪練等”的按鈕後, 等級會增加1等. 要完成這個功能不難, 只需要加入Button的按鈕, 並在action部分加入增加等級的指令即可.

完成的程式部分如下:

struct ContentView: View {


@State var lv = 1

var body: some View {
VStack (spacing: 30){
Text("等級: \(lv)")

Button(action: {self.lv = self.lv + 1}) {
Text("打怪練等")
}
}

}
}

我們試玩了一下, 人物的等級的確會隨著每次按下”打怪練等”按鈕後增加1等. 這時傳來老媽的聲音, 告知要吃晚餐了. 看著剛剛辛苦的練等, 人物等級終於達到10等, 我們心滿意足的關閉App, 前去享用晚餐.

吃完飯後, 正當我們打開App, 想繼續打怪練等時, 想不到人物等級居然又變回了1等, 這表示吃飯前辛苦的成果付諸流水, 這到底是什麼問題呢?

原因是我們在程式碼的一開始就給定了人物等級為1的設定(var lv = 1), 所以在每次重新執行程式時, 人物的等級就會變回1. 這樣表示你必須在不吃不喝(不能關閉App)的情況下, 才有機會完成所有關卡. 為了解決這個問題, 我們必須使用UserDefault這個關鍵字, 來幫助我們存檔做紀錄.

修改後的程式如下:

struct ContentView: View {

@State var lv = UserDefaults.standard.integer(forKey: "level")

var body: some View {
VStack (spacing: 30){
Text("等級: \(lv)")

Button(action: {
self.lv = self.lv + 1
UserDefaults.standard.set(self.lv, forKey: "level")
}) {
Text("打怪練等")
}
}

}
}

一樣在一開始宣告一個變數為lv, 但這邊透過 UserDefaults.standard.integer的方式來讀取一個整數放入lv. 使用者也許會有很多整數要儲存, 所以這邊要多增加一個標籤level, 讓系統知道要讀取哪個整數放入lv的變數中.

在按下按鈕後的action中, 我們除了要讓等級提升1等外, 還必須要把這個值存放起來, 這邊是透過UserDefaults.standard.set的方式, 來把值儲存起來. 使用者可能會有很多值要儲存, 所以一樣得透過一個標籤來讓系統知道是把等級lv這個變數的值, 存放在標籤level中.

如果你有時候執行這段程式法, 你會發現一個問題, 雖然在關閉App後人物的等級能夠儲存起來, 但是為什麼在最一開始的人物等級是0不是1?

原因是第一次開啟App的時候, 系統嘗試找標籤為level的整數數值, 因為找不到, 所以回覆一個預設值為0的數值給lv變數. 由於等級為0有點不直覺, 為了解決這個問題, 我可以透過UserDefaults.standard.register這個語法來修改預設值. 並把這段程式碼放入AppDelegate這個檔案中. 下面這段程式碼可以解讀成, 當讀取標籤為level的數值時, 如果讀取不到, 則回傳1.

UserDefaults.standard.register(defaults: ["level" : 1]) // 修改默認值

接著, 我們來看第二個範例, 透過UserDefault來儲存字串. 我們來實作一個能夠紀錄按下Button時間的App, 並且下次開啟App時, 仍然能夠顯示之前紀錄的時間.

struct ContentView: View {

@State var currentTime = UserDefaults.standard.string(forKey: "time")

var body: some View {

VStack (spacing: 30){

Text("紀錄: \(currentTime!)")

Button(action: {

let formatter = DateFormatter()
formatter.dateFormat = "HH:mm:ss"
self.currentTime = formatter.string(from: Date())

UserDefaults.standard.set(self.currentTime, forKey: "time")

}) {
Text("更新時間")
}

}

}
}

我們使用UserDefaults.standard.string的方法, 透過標籤time來讀取一個字串, 並把讀到的結果傳入變數currentTime中.

當按下Button後, 會利用DateFormatter的函式, 把目前讀到的時間轉成我們要的時間格式字串 (HH:mm:ss). 並且把值更新給currentTime這個變數. 最後, 再使用UserDefaults.standard.set, 透過標籤time來更新裡面存放的字串.

我們一樣得在AppDelegate的檔案中, 告訴系統如果透過標籤time找不到值的時候該怎麼辦. 這邊我們設定讀不到值時, 回傳一個None的字串.

UserDefaults.standard.register(defaults: ["time" : "None"]) // 提供初始值

在這邊其實有另一個寫法. 還記得當初我們在AppDelegate的檔案中加入預設值的原因嗎? 那是因為透過UserDefaults.standard.integer讀取整數時, 在讀不到值時, Swift會自動回傳一個0給我們.

但是如果我們是透過UserDefaults.standard.string來讀字串, 當讀不到值時, Swift並不會聰明的給定一個預設值.

換句話說, 如果是UserDefaults.standard.integer讀整數時, 回傳一定是Int. 但是如果是UserDefaults.standard.string讀字串時, 回傳會變成String?, 會是一個optional的狀態, 所以我們也可以寫成下面這種方式, 透過 ?? 的方式來給予讀不到時的預設值, 這樣一來, 也不需要額外在修改AppDelegate的內容. 而且也因為current的屬性從String?轉為String, 在程式中使用currentTime時, 便可以把強迫取值的驚嘆號!給移除.

struct ContentView: View {

@State var currentTime = UserDefaults.standard.string(forKey: "time") ?? "None"

var body: some View {

VStack (spacing: 30){

Text("紀錄: \(currentTime)")

Button(action: {

let formatter = DateFormatter()
formatter.dateFormat = "HH:mm:ss"
self.currentTime = formatter.string(from: Date())

UserDefaults.standard.set(self.currentTime, forKey: "time")

}) {
Text("更新時間")
}

}

}
}

--

--

KuoJed
彼得潘的 Swift iOS / Flutter App 開發教室

「沒有一件你努力過的事是白費的。」 當你這麼相信,並且實踐,就會成真。