一直木有看過這個細節,用UserDefaults是能不能存復雜一點的對象。大家可能都看到過UserDefaults的一個方法setObject: forKey:
,用這個方法存過NSDictionary
,NSArray
什么的,也存過字符串。
偶然一次直接存了一個繼承自JSONModel
的實體類,然后就悲劇了。后來查了下蘋果的文檔:
The value parameter can be only property list objects: NSData, NSString, NSNumber, NSDate, NSArray, or NSDictionary. For NSArray and NSDictionary objects, their contents must be property list objects.
簡單來說就是setObject:forKey:
方法可以存NSData
,NSString
什么的對象,即使是NSDictionary
和NSArray
內存放的元素也必須是property list objects的。具體什么是property list object看這里。關於JSONModel
可以看這里,還不錯。
既然蘋果的API已經限制到這個地步了再想別的已經玩不出什么花樣了。是的,你可以存文件。不過這里說的還是用UserDefaults嘛。
解決這個問題的核心思想就是把一個對象轉換為NSData
,或者說是序列化為NSData
。序列化的說法不一定准確但是存在這樣的一個過程,具體的后面再細說。當一個對象可以轉化為NSData
了也就適用NSUserDefaults
的方法setObject: forKey:
了。也就是這樣的用法:
//假設有一個用戶實體類 class UserModel { var userId: String = "" var accessToken: String = "" } //然后 let userModel = UserModel() //正式開始 let userDefaults = NSUserDefaults.standardUserDefaults() let encodedObject = NSKeyedArchiver.archivedDataWithRootObject(object) userDefaults.setObject(encodedObject, forKey: "UserInfoKey") userDefaults.synchronize() //最后不要忘了這個
大體的意思在上面的代碼中全部都體現出來了。但是如果運行上面的代碼肯定是會出錯的。
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Attempt to insert non-property list object UserModel for key UserInfoKey'
因為不是property list object所以執行方法setObject:forKey
的時候App直接Crash。
這個問題看似就在property list object上了。但是回到什么說的,我們的思路是把這個自定義的實體類的對象轉化為NSData
。這個時候就要用到NSKeyedArchiver
和NSKeyedUnarchiver
,這也就間接的用到了NSCoding
接口。因為一個實體類如果沒有實現NSCoding
那么在NSKeyedArchiver
和NSKeyedUnarchiver
上還是會出錯的。
對上面的代碼做一次小小的改進:
class WeiboUserModel: NSObject, NSCoding { //1 struct PropertyKey { static let userIdKey = "userId" static let accessTokenKey = "accessToken" static let expirationDateKey = "expirationDate" static let refreshTokenKey = "refreshToken" } var userId: String? var accessToken: String? var expirationDate: NSDate? var refreshToken: String? func encodeWithCoder(aCoder: NSCoder) { //2 aCoder.encodeObject(userId, forKey: PropertyKey.userIdKey) aCoder.encodeObject(accessToken, forKey: PropertyKey.accessTokenKey) aCoder.encodeObject(expirationDate, forKey: PropertyKey.expirationDateKey) aCoder.encodeObject(refreshToken, forKey: PropertyKey.refreshTokenKey) } required init?(coder aDecoder: NSCoder) { // 3 userId = aDecoder.decodeObjectForKey(PropertyKey.userIdKey) as? String accessToken = aDecoder.decodeObjectForKey(PropertyKey.accessTokenKey) as? String expirationDate = aDecoder.decodeObjectForKey(PropertyKey.expirationDateKey) as? NSDate refreshToken = aDecoder.decodeObjectForKey(PropertyKey.refreshTokenKey) as? String } }
如此的修改就可以讓他們跑起來了。下面依次解釋:
1. 實現NSObject
和NSCoding
。NSObject
可以不加,用@objc
修飾某些方法也可以。NSCoding
接口提供了序列化和反序列化對象的時候的編解碼方法。
UserModel
的類名稱修改 為WeiboUserModel
。這部分代碼是整個項目的一部分,后面會補齊。
2. 在序列化一個對象的時候使用方法func encodeWithCoder(aCoder: NSCoder)
編碼。
3. 反序列化的時候用方法init?(coder aDecoder: NSCoder)
解碼。
在大體邏輯不修改的條件下,我們看下完整的可以存實體類對象的代碼。
//然后 let userModel = WeiboUserModel() //正式開始 let userDefaults = NSUserDefaults.standardUserDefaults() let encodedObject = NSKeyedArchiver.archivedDataWithRootObject(object) userDefaults.setObject(encodedObject, forKey: "UserInfoKey") userDefaults.synchronize() //最后不要忘了這個
這樣就可以運行了。但是我們不能止步於此。因為如果項目中需要保存的地方太多的時候,到處都寫滿了(極有可能是復制粘貼)NSUserDefaults
實例的調用。這樣的代碼太過僵化。而且很容易忘記最后的userDefaults.synchronize ()
調用。這會導致對象的存儲出問題。
所以我們要對這一部分的代碼做一定的封裝:
extension NSUserDefaults { //1 func saveCustomObject(customObject object: NSCoding, key: String) { //2 let encodedObject = NSKeyedArchiver.archivedDataWithRootObject(object) self.setObject(encodedObject, forKey: key) self.synchronize() } func getCustomObject(forKey key: String) -> AnyObject? { //3 let decodedObject = self.objectForKey(key) as? NSData if let decoded = decodedObject { let object = NSKeyedUnarchiver.unarchiveObjectWithData(decoded) return object } return nil } }
我們把存取的方法都放在NSUserDefaults
的擴展里。這樣用戶在使用的時候就可以和使用NSUserDefaults
本身的方法一樣的了。而且synchronize()
方法也封裝在里面了,再也不用擔心忘記d對象沒有存上了。來看看調用的一個小細節。
userDefaults.saveCustomObject(customObject: userModel, key: "UserInfoKey") //存 userDefaults.getCustomObject("UserInfoKey") as? WeiboUserModel //取
好的,到這。完整項目的代碼在這里
to be continued