建议CV,Swift中使用UserDefault的一点经验

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

本文同时参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。

前言

在日常开发中,我们总是会使用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<T: Codable>(_ 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<T: Codable>(_ 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)
    }
}
复制代码

其实这个分类的思路非常简单:

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开发者常用的工具,我觉得我已经尽力分享了,大家喜欢请踊跃点赞、留言,感谢!

我们下期见。

猜你喜欢

转载自juejin.im/post/7018723847155236877
今日推荐