Swift: 用UserDefaults保存復雜對象


一直木有看過這個細節,用UserDefaults是能不能存復雜一點的對象。大家可能都看到過UserDefaults的一個方法setObject: forKey:,用這個方法存過NSDictionaryNSArray什么的,也存過字符串。

偶然一次直接存了一個繼承自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:方法可以存NSDataNSString什么的對象,即使是NSDictionaryNSArray內存放的元素也必須是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。這個時候就要用到NSKeyedArchiverNSKeyedUnarchiver,這也就間接的用到了NSCoding接口。因為一個實體類如果沒有實現NSCoding那么在NSKeyedArchiverNSKeyedUnarchiver上還是會出錯的。

對上面的代碼做一次小小的改進:

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. 實現NSObjectNSCodingNSObject可以不加,用@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

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM