建議CV,Swift中使用UserDefault的一點經驗

語言: CN / TW / HK

highlight: tomorrow-night-blue

小知識,大挑戰!本文正在參與“程式設計師必備小知識”創作活動。

本文同時參與「掘力星計劃」,贏取創作大禮包,挑戰創作激勵金。

前言

在日常開發中,我們總是會使用UserDefault儲存一些輕量的資料。

而對於UserDefault大家可能是經常用,但是就是不知道它到底是一個怎麼樣的型別,資料是怎麼儲存的呢?

今天我將詳細為大家介紹一下我的一點經驗。

UserDefault資料儲存的路徑

首先我們看看UserDefault在模擬器下面的路徑:

image.png

因為一般情況下,我們不好看真機中App的沙盒資料夾,所以真機的路徑,我打印出來地址:

/var/mobile/Containers/Data/Application/961391DA-4E5B-44E8-A38A-DE7A5F5CBC15/Library/Preferences/com.lostsakura.www.RxStudy.plist

小結

  • 請記住這個結論:UserDefault儲存的資料儲存在App沙盒中/Library/Preferences/的資料夾下面

  • 該檔案是一個plist檔案

  • 檔名是Bundle identifier中配置設定的名稱,大家看我的專案配置截圖就可以發現了:

image.png

  • 路徑列印的方法大家可以參考下面程式碼塊:

    ``` guard let bundleIdentifier = Bundle.main.bundleIdentifier else { return }

    let path = NSHomeDirectory() + "/Library" + "/Preferences" + "/(bundleIdentifier)" + ".plist" print(path) ```

既然是.plist檔案,那麼它能存什麼呢

既然它是.plist檔案,那麼我覺得稍微有點iOS開發經歷的人就知道它可以儲存什麼了——儲存物件。

通過UserDefault的原始碼註釋,我們也可以找到詳細的說明:

Key-Value Store: NSUserDefaults stores Property List objects (NSString, NSData, NSNumber, NSDate, NSArray, and NSDictionary) identified by NSString keys, similar to an NSMutableDictionary.

你也許會好奇,在Swift中,已經很少使用帶NS開頭的資料型別了,那麼這裡的註釋何為說儲存的型別是這些呢,原因有二:

  • UserDefault是針對Swift的,NSUserDefault是針對OC的,其實就是一個類,只是命名不同而已

  • 在Swift中使用UserDefault,系統內部會對其進行自動型別轉換,讓使用者沒有過多的感知型別轉換

| Swift | OC | | --- | --- | |String |NSString| |Data |NSData| |Int/Double/Float/Bool |NSNumber| |Date |NSDate| |Array |NSArray| |Dictionary|NSDictionary|

大家牢記這一個點就可以了,UserDefault實際儲存的是資料是物件,是物件,是物件。

UserDefault的API呼叫注意事項

我們先摘抄Swift和OC幾個相同功能的API來對比一下:

| Swift | OC | | --- | --- | |func object(forKey defaultName: String) -> Any? |- (nullable id)objectForKey:(NSString )defaultName; |func set(_ value: Any?, forKey defaultName: String)|- (void)setObject:(nullable id)value forKey:(NSString )defaultName; |func integer(forKey defaultName: String) -> Int|- (NSInteger)integerForKey:(NSString )defaultName; |func set(_ value: Int, forKey defaultName: String)|- (void)setInteger:(NSInteger)value forKey:(NSString )defaultName; |更多同種API,可以自行在原始碼中檢視

我們可以看到,在Swift和OC中,API基本都相同並且一一對應。

其中setObjectobjectForKey可以說是UserDefault中最基本的方法,通過鍵值對去儲存和讀取資料。而其他set和get方法都是對這個方法的一個型別轉換封裝。

Swift中的呼叫

我們先用Swift的setObjectobjectForKey呼叫一把試試,注意我呼叫的API。

image.png

然後你會發現,返回的getAge會有一個警告,說getAge是一個Any?型別:

image.png

我們看看列印結果:

Optional(10)

但是如果我們要從真正意義上的去確定getAge的型別,我們必須解包一把,有點繁瑣:

``` guard let getAge = UserDefaults.standard.object(forKey: "age") as? Int else { return }

print(getAge)

10 ```

此時,func set(_ value: Int, forKey defaultName: String)func integer(forKey defaultName: String) -> Int的API才顯示其價值。

我們來換換API再試試:

``` let age = 10

UserDefaults.standard.set(age, forKey: "age")

let getAge = UserDefaults.standard.integer(forKey: "age")

print(getAge) ```

其實你會發現,我對UserDefaults.standard.set(age, forKey: "age")這個API並沒有做更改,或者說我更改了API還是同名函式,改動了依舊看不出來:

image.png

但是我將UserDefaults.standard.value(forKey: "age")改成了UserDefaults.standard.integer(forKey: "age"),整個效果就不同了。

integer(forKey:)這個方法相當於顯式的說明讀取的這個值型別是Int型別,並且不是可選。

我們試著,不去儲存age,直接通過key“age”讀取,看看返回的是什麼,嗯,為了驗證這個,我先要把我的App刪除再重新執行:

image.png

和我預期的一致,沒有通過key-value儲存,而直接通過key獲取值,因為考慮到integer(forKey:)返回的是一個Int型別,而非可選,所以是預設值0

想想如果這裡不使用integer(forKey:),而是object(forKey:)返回的是什麼呢?看看上面的API列表,我想你應該知道答案了。

OC中的呼叫

我個人覺得OC中會有點讓人感覺麻煩,不如我們接著往下看:

``` NSNumber *age = @10;

[NSUserDefaults.standardUserDefaults setObject:age forKey:@"age"];

NSNumber *getAge = [NSUserDefaults.standardUserDefaults objectForKey:@"age"];

NSLog(@"%@",getAge);

10 ```

由於OC中API明確了說setObject:(nullable id)value是一個id類,必須是類,所以我們不能用NSInteger,而選用了NSNumber。

你需要記住的是OC中NSInteger根本就不是一個類,它是 typedef long NSInteger,而Swift中Int是一個結構體。

我們也可以在OC換換API去試著執行一下:

``` NSInteger age = 10;

/// 使用setInteger,所以age可以定義為NSInteger [NSUserDefaults.standardUserDefaults setInteger: age forKey:@"age"];

NSInteger getAge = [NSUserDefaults.standardUserDefaults integerForKey:@"age"];

NSLog(@"%ld",getAge); ``` 這樣一切都正常了。

小結

在Swift和OC中使用UserDefault,setObjectobjectForKey最好少用,因為此時不管儲存還是讀取的都是Any? | nullable id型別,型別不明確,而且可以為nil,非常的不可靠。

建議明確資料的型別,並使用set與xxx(forKey:)對應的方法,這樣會有明確的值,而且就算沒有set,xxx(forKey:)也會有預設值。

同時需要注意的是setObjectobjectForKey所有其他具體型別API的基礎API,我們通過其原始碼實現可以看出:

``` open func set(_ value: Int, forKey defaultName: String) {

set(NSNumber(value: value), forKey: defaultName)

} ```

``` open func integer(forKey defaultName: String) -> Int { guard let aVal = object(forKey: defaultName) else { return 0 }

if let bVal = aVal as? Int {
    return bVal
}

if let bVal = aVal as? String {
    return NSString(string: bVal).integerValue
}

return 0

} ```

為UserDefaults配置初始值

上面我們講到,如果我們沒有對age這個值進行寫,那麼預設讀出來的值是0。

``` let getAge = UserDefaults.standard.integer(forKey: "age")

print(getAge)

0 ```

可是有的時候,我們希望這個age的預設值不是0,而是我們初始化配置好的一個值,我們該怎麼做呢?

這個API給了大家一些可能:

registerDefaults: adds the registrationDictionary to the last item in every search list. This means that after NSUserDefaults has looked for a value in every other valid location, it will look in registered defaults, making them useful as a "fallback" value. Registered defaults are never stored between runs of an application, and are visible only to the application that registers them.

Default values from Defaults Configuration Files will automatically be registered.

open func register(defaults registrationDictionary: [String : Any])

``` let userDefaults = UserDefaults.standard userDefaults.register( defaults: [ "enabledSound": true, "enabledVibration": true ] )

userDefaults.bool(forKey: "enabledSound") // true ```

需要注意的是register的值會被有效寫入,有且僅當key為nil時。

我們接著看下面這段程式碼:

``` let userDefaults = UserDefaults.standard

userDefaults.set(false, forKey: "enabledSound") // false

userDefaults.register( defaults: [ "enabledSound": true, "enabledVibration": true ] )

userDefaults.bool(forKey: "enabledSound") // false ```

你看,我先通過userDefaults.set對keyenabledSound設定了false,然後用通過userDefaults.register註冊了keyenabledSound的預設值為true,結果最後我通過userDefaults.bool獲取到的值還是false。

register方法並沒有生效。

如果感興趣,可以看看這篇文章

UserDefault與Codable結合使用,編寫分類

雖然UserDefault中的註釋已經說明了,它只能儲存基本的資料型別:NSString, NSData, NSNumber, NSDate, NSArray, and NSDictionar,但是有的時候我就是想儲存一個Model資料應該怎麼辦呢?

我想下面這個UserDefault+Codable你可能用的上:

``` extension UserDefaults {     /// 遵守Codable協議的set方法     ///     /// - Parameters:     ///   - object: 泛型的物件     ///   - key: 鍵     ///   - encoder: 序列化器     public func setCodableObject(_ object: T, forKey key: String, usingEncoder encoder: JSONEncoder = JSONEncoder()) {

let data = try? encoder.encode(object)         set(data, forKey: key)     }

/// 遵守Codable協議的get方法     ///     /// - Parameters:     ///   - type: 泛型的型別     ///   - key: 鍵     ///   - decoder: 反序列器     /// - Returns: 可選型別的泛型的型別物件     public func getCodableObject(_ type: T.Type, with key: String, usingDecoder decoder: JSONDecoder = JSONDecoder()) -> T? {         guard let data = value(forKey: key) as? Data else { return nil }         return try? decoder.decode(type.self, from: data)     } } ```

其實這個分類的思路非常簡單:

```mermaid graph TD Model --> Data --> UserDefaults中呼叫Data的set和get ```

使用起來也非常的簡單:

``` struct Person { let name: String let age: Int }

let person = Person(name: "season", age: 10)

UserDefaults.standard.setCodableObject(person, forKey: "person")

let getPerson = UserDefaults.standard.getCodableObject(Person.self, with: "person") ```

OC中不能使用Codable協議,不過Model轉NSData的方法,還是有可以替換的,大家可以考慮使用NSCoding協議。

UserDefault像Dictionary一樣進行讀和寫

我們都知道字典型別的可以通過dict["key"]的形式進行讀和寫,比如下面這樣:

``` var dict = ["age": 10]

print(dict["age"])

dict["age"] = 20

print(dict["age"]) ```

其實通過對UserDefault新增subscript擴充套件,UserDefault也可以像Dictionary一樣進行讀與寫。

分類程式碼如下:

``` extension UserDefaults {

/// 針對Any?

public subscript(key: String) -> Any? {         get {             return object(forKey: key)         }         set {             set(newValue, forKey: key)         }     }

/// 針對Int

public subscript(int key: String) -> Int {         get {             return integer(forKey: key)         }

set {             set(newValue, forKey: key)         }     } }

/// 更多具體型別的擴充套件可以自己編寫 ```

注意,subscript函式不能重名,所以我在針對Int的儲存與讀取時,函式變成了subscript(int key: String)

使用,讀寫name的時候呼叫的是subscript(key: String),而讀寫age的時候呼叫的是subscript(int key: String),大家需要仔細觀察喔:

``` UserDefaults.standard["name"] = "season"

print(UserDefaults.standard["name"])

UserDefaults.standard[int: "age"] = 10

print(UserDefaults.standard[int: "age"])

Optional(season)

10 ```

如果想要更多型別的讀寫都支援這種寫法,接著寫對應型別的擴充套件即可。

儘量不要使用硬編碼

大家可以看到,UserDefault進行讀寫的時候,我們經常要打交道的就是key,而key是字串,是硬編碼。

考慮使用UserDefault進行讀寫一般都是經常要使用的資料,我建議最好是把這個key進行常量化,單獨建立一個檔案,將其常量化,比如我要用UserDefault儲存使用者名稱,我就這麼寫:

/// 儲存使用者名稱的key let kUsername = "kUsername"

呼叫的時候這樣就行了:

``` let name = "season"

UserDefaults.standard.set(name, forKey: kUsername) ```

synchronize方法不用寫了

我們經常在使用的UserDefault會在set之後呼叫synchronize方法,比如這樣:

``` UserDefaults.standard.set(10, forKey: "age")

UserDefaults.standard.synchronize() ```

特別是一些老專案,抑或是OC的以及從OC轉到Swift編寫程式碼的時候。

synchronize()考慮的是一種同步安全,但是現在已經可以不寫了,官方原始碼註釋如下:

-synchronize is deprecated and will be marked with the API_DEPRECATED macro in a future release.

本著能少寫一點程式碼是一點程式碼的思路,還是別寫了吧。

參考文件

swift-corelibs-foundation/Sources/Foundation/UserDefaults.swift

Setting default values for NSUserDefaults

總結

本文從UserDefault的資料儲存路徑開始探索,發現UserDefault的儲存資料型別是plist檔案,進而知道了UserDefault儲存的資料型別,並通過對比Swift和OC的API呼叫對比,說明了使用過程中的注意事項和配置預設值的方法。

另外通過對UserDefault進行擴充套件,讓它獲得了對比較大的資料的讀寫能力和像字典方式讀寫的呼叫方式。

synchronize方法通過註釋我們也可以知道它不用特地去寫了。

最後我還是要強調一點,UserDefault適合儲存輕資料,使用的時候請酌情處理,如果在UserDefault中讀寫過大的檔案,會影響App的效能和體驗。

今天我分享的特別多,UserDefault作為iOS開發者常用的工具,我覺得我已經盡力分享了,大家喜歡請踴躍點贊、留言,感謝!

我們下期見。