阅读原文
前段时间写了一个macOS图床小应用cuImage,里面有些技术点一直没抽出时间总结和分享。临近毕业事情也比较多,只能挤时间了。本文将利用Swift的反射机制遍历对象属性,从而简化代码,提高代码复用率。
cuImage中每个图床都有相应的配置信息,如七牛云的图床配置信息QiniuHostInfo
。我希望将其通过UserDefaults
保存起来。(图床中有些敏感信息需要加密,但为了简化描述,本文就不涉及加密相关的点了,其实加密方面我也是小白菜。至于为什么不用Keychain
加密就是另一回事了,这里略过。)
由于UserDefaults
不支持自定义对象,如果强行直接保存,会在运行时抛出异常:
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Attempt to insert non-property list object 'Abc' for key 'Xyz'.
为了让自定义对象也能够通过UserDefaults
进行持久化存储,需要通过NSKeyedArchiver
将自定义对象编码成NSData格式。这就需要遵循NSCoding
协议,实现该协议声明的两个方法(init(_:)
和encode(_:)
)。一般的实现方式如下:
final class QiniuHostInfo: NSObject, NSCoding {
var name = ""
var accessKey = ""
var secretKey = ""
init(name: String = "", accessKey: String = "", secretKey: String = "") {
self.name = name
self.accessKey = accessKey
self.secretKey = secretKey
super.init()
}
init?(coder aDecoder: NSCoder) {
name = aDecoder.decodeObject(forKey: #keyPath(name)) as! String
accessKey = aDecoder.decodeObject(forKey: #keyPath(accessKey)) as! String
secretKey = aDecoder.decodeObject(forKey: #keyPath(secretKey)) as! String
}
func encode(with aCoder: NSCoder) {
aCoder.encode(name, forKey: #keyPath(name))
aCoder.encode(accessKey, forKey: #keyPath(accessKey))
aCoder.encode(secretKey, forKey: #keyPath(secretKey))
}
}
final class ImgurHostInfo: NSObject, NSCoding {
var name = ""
var userName = ""
var password = ""
init(name: String = "", userName: String = "", password: String = "") {
self.name = name
self.userName = userName
self.password = password
super.init()
}
init?(coder aDecoder: NSCoder) {
name = aDecoder.decodeObject(forKey: #keyPath(name)) as! String
userName = aDecoder.decodeObject(forKey: #keyPath(userName)) as! String
password = aDecoder.decodeObject(forKey: #keyPath(password)) as! String
}
func encode(with aCoder: NSCoder) {
aCoder.encode(name, forKey: #keyPath(name))
aCoder.encode(userName, forKey: #keyPath(userName))
aCoder.encode(password, forKey: #keyPath(password))
}
}
这种方式的缺点在于,如果QiniuHostInfo
增加新的属性或者重构代码时修改属性,需要在NSCoding
声明的两个方法中都添加或修改对应的代码,还可能有疏漏。而且,当需要新增一个图床(如ImgurHostInfo
)时,又得按相同的的步骤撸一遍代码。
下面是我最终采用的方式。首先,我为各种图床信息类写了个公共基类HostInfo
,将实现NSCoding
协议方法的任务交给了基类HostInfo
,一次性搞定。以后每增加一个图床,只需要在相应的图床信息子类(如QiniuHostInfo
, ImgurHostInfo
)定义好自身的属性就行了,这样子类的代码就清爽多了。
class HostInfo: NSObject, NSCoding {
var name = ""
override init() {
super.init()
}
convenience required init?(coder aDecoder: NSCoder) {
self.init()
forEachChildOfMirror(reflecting: self) { key in
setValue(aDecoder.decodeObject(forKey: key), forKey: key)
}
}
func encode(with aCoder: NSCoder) {
forEachChildOfMirror(reflecting: self) { key in
aCoder.encode(value(forKey: key), forKey: key)
}
}
func forEachChildOfMirror(reflecting subject: Any, handler: (String) -> Void) {
var mirror: Mirror? = Mirror(reflecting: subject)
while mirror != nil {
for child in mirror!.children {
if let key = child.label {
handler(key)
}
}
// Get super class's properties.
mirror = mirror!.superclassMirror
}
}
}
final class QiniuHostInfo: HostInfo {
var accessKey = ""
var secretKey = ""
}
final class ImgurHostInfo: HostInfo {
var userName = ""
var password = ""
}
上面的代码中,init(_:)
和encode(_:)
都需要用到反射,所以我将公共部分抽出来,并把每访问到一个属性时要做的操作以闭包的形式传给了forEachChildOfMirror(_:_:)
函数。其中用到了superclassMirror
是因为在父类中可能也有一些公共属性需要被遍历到,如HostInfo
中的name
属性。于是遍历完当前类的属性后,就可以通过superclassMirror
顺着继承链往上爬,访问到继承连上的类的所有属性了。
如果读者们有其他不错的实现方式,期待你们的分享。