前段時(shí)間寫了一個(gè)macOS圖床小應(yīng)用cuImage考阱,里面有些技術(shù)點(diǎn)一直沒(méi)抽出時(shí)間總結(jié)和分享葬凳。臨近畢業(yè)事情也比較多突琳,只能擠時(shí)間了他巨。本文將利用Swift的反射機(jī)制遍歷對(duì)象屬性,從而簡(jiǎn)化代碼蒂秘,提高代碼復(fù)用率泽本。
cuImage中每個(gè)圖床都有相應(yīng)的配置信息,如七牛云的圖床配置信息QiniuHostInfo
材彪。我希望將其通過(guò)UserDefaults
保存起來(lái)。(圖床中有些敏感信息需要加密琴儿,但為了簡(jiǎn)化描述段化,本文就不涉及加密相關(guān)的點(diǎn)了,其實(shí)加密方面我也是小白菜造成。至于為什么不用Keychain
加密就是另一回事了显熏,這里略過(guò)。)
由于UserDefaults
不支持自定義對(duì)象晒屎,如果強(qiáng)行直接保存喘蟆,會(huì)在運(yùn)行時(shí)拋出異常:
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Attempt to insert non-property list object 'Abc' for key 'Xyz'.
為了讓自定義對(duì)象也能夠通過(guò)UserDefaults
進(jìn)行持久化存儲(chǔ),需要通過(guò)NSKeyedArchiver
將自定義對(duì)象編碼成NSData格式鼓鲁。這就需要遵循NSCoding
協(xié)議蕴轨,實(shí)現(xiàn)該協(xié)議聲明的兩個(gè)方法(init(_:)
和encode(_:)
)。一般的實(shí)現(xiàn)方式如下:
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))
}
}
這種方式的缺點(diǎn)在于骇吭,如果QiniuHostInfo
增加新的屬性或者重構(gòu)代碼時(shí)修改屬性橙弱,需要在NSCoding
聲明的兩個(gè)方法中都添加或修改對(duì)應(yīng)的代碼,還可能有疏漏燥狰。而且棘脐,當(dāng)需要新增一個(gè)圖床(如ImgurHostInfo
)時(shí),又得按相同的的步驟擼一遍代碼龙致。
下面是我最終采用的方式蛀缝。首先,我為各種圖床信息類寫了個(gè)公共基類HostInfo
目代,將實(shí)現(xiàn)NSCoding
協(xié)議方法的任務(wù)交給了基類HostInfo
屈梁,一次性搞定嗤练。以后每增加一個(gè)圖床,只需要在相應(yīng)的圖床信息子類(如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(_:)
都需要用到反射真朗,所以我將公共部分抽出來(lái)此疹,并把每訪問(wèn)到一個(gè)屬性時(shí)要做的操作以閉包的形式傳給了forEachChildOfMirror(_:_:)
函數(shù)。其中用到了superclassMirror
是因?yàn)樵诟割愔锌赡芤灿幸恍┕矊傩孕枰槐闅v到遮婶,如HostInfo
中的name
屬性蝗碎。于是遍歷完當(dāng)前類的屬性后,就可以通過(guò)superclassMirror
順著繼承鏈往上爬旗扑,訪問(wèn)到繼承連上的類的所有屬性了蹦骑。
如果讀者們有其他不錯(cuò)的實(shí)現(xiàn)方式,期待你們的分享臀防。