NSKeyedUnarchiver.unarchiveTopLevelObjectWithData return nil

房东的猫 提交于 2021-02-11 15:14:16

问题


I want to save an array of objects into UserDefaults and load it back.

When trying to unarchive the data it always returns nil.. any idea?

This is my object:

class DbInsideLevel: NSObject, NSSecureCoding {
    
    static var supportsSecureCoding: Bool {
        return true
    }
    
    let idNum: Int!
    var topicId: Int = 0
    var tryCount: Int = 0
    var score: Int = 0
    var isOpen: Bool = false
    var lastPlayedDate: Date?
    
    init(idNum: Int, topicId: Int, tryCount: Int = 0, score: Int = 0, open: Bool, lastPlayedDate: Date?) {
        self.idNum = idNum
        self.topicId = topicId
        self.tryCount = tryCount
        self.score = score
        self.isOpen = open
        self.lastPlayedDate = lastPlayedDate
    }
    
    convenience required init?(coder: NSCoder) {
        
        guard
            let idNum       = coder.decodeObject(forKey: "idNum") as? Int,
            let topicId     = coder.decodeObject(forKey: "topicId") as? Int,
            let tryCount    = coder.decodeObject(forKey: "tryCount") as? Int,
            let score       = coder.decodeObject(forKey: "score") as? Int,
            let open        = coder.decodeObject(forKey: "isOpen") as? Bool,
            let lastPlayed  = coder.decodeObject(forKey: "lastPlayedDate") as? Date
        else {
            return nil
        }
        
        self.init(idNum: idNum, topicId: topicId, tryCount: tryCount, score: score, open: open, lastPlayedDate: lastPlayed)
    }
    
    func encode(with coder: NSCoder) {
        coder.encode(idNum,             forKey: "idNum")
        coder.encode(topicId,           forKey: "topicId")
        coder.encode(tryCount,          forKey: "tryCount")
        coder.encode(score,             forKey: "score")
        coder.encode(isOpen,            forKey: "isOpen")
        coder.encode(lastPlayedDate,    forKey: "lastPlayedDate")
    }

    func update(score: Int) {
        
        //  Update score if necessary
        if score > self.score {
            self.score = score
        }
        
        //  Increment try count
        self.tryCount = self.tryCount + 1
    }
}

Archiving the data:

func archiveData(with object: Any, to key: String) -> Data? {
    do {
        guard
            let data = try? NSKeyedArchiver.archivedData(withRootObject: object, requiringSecureCoding: true) else {
            return nil
        }
        
        return data
    }
}

Unarchiving the data:

func unarchiveData(data: Data) -> Any? {
    
    do {
        let unarchivedData = try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as? [DbInsideLevel]
        return unarchivedData
    } catch {
        return nil
    }
}

Saving to UserDefaults:

class func americanSaveContext(data: [DbInsideLevel]) {
    if data.count != 0 {
        if let archived = MainDb.sharedInstance.archiveData(with: data, to: AppConstants.Keys.UserDefaults.American) {
            MainDb.sharedInstance.dataStore(data: archived)
        }
    }
}

Loading from UserDefaults:

class func americanDataRetrive(topicId: Int) -> [DbInsideLevel]? {
    if let data = MainDb.sharedInstance.dataRetrive() {
        let unarchived = MainDb.sharedInstance.unarchiveData(data: data) as! [DbInsideLevel]
        return unarchived
    }
    
    return nil
}

UserDefaults helpers for saving / loading:

extension MainDb {
    
    //  MARK: - UserDefaults helpers
    
    func dataRetrive() -> Data? {
        let defaults = UserDefaults.standard
        return defaults.value(forKey: AppConstants.Keys.UserDefaults.American) as? Data
    }
    
    func dataStore(data: Data) {
        let defaults = UserDefaults.standard
        defaults.setValue(data, forKey: AppConstants.Keys.UserDefaults.American)
    }
    
    func dataReset() {
        let defaults = UserDefaults.standard
        defaults.removeObject(forKey: AppConstants.Keys.UserDefaults.American)
    }
}

回答1:


Your issue there is that you are using the wrong method to decode your integers and your boolean. I would also implement the required decoder instead of a convenience initializer.

required init(coder decoder: NSCoder) {
    self.idNum = decoder.decodeInteger(forKey: "idNum")
    self.topicId = decoder.decodeInteger(forKey: "topicId")
    self.tryCount = decoder.decodeInteger(forKey: "tryCount")
    self.score = decoder.decodeInteger(forKey: "score")
    self.isOpen = decoder.decodeBool(forKey: "isOpen")
    self.lastPlayedDate = decoder.decodeObject(forKey: "lastPlayedDate") as? Date
}

Note that you can also use Swift 4 or later Codable protocol to encode and decode your custom classes/structures and save them as data inside UserDefaults or as a plist file inside your application support folder. Last but not least don't use setValue and value(forKey:) to save and retrieve your data, user defaults has a specific method for retrieving data called data(forKey:) and set(_ value: Any) to persist your data:

extension UserDefaults {
    func decode<T: Decodable>(_ type: T.Type, forKey defaultName: String) throws -> T {
        try JSONDecoder().decode(T.self, from: data(forKey: defaultName) ?? .init())
    }
    func encode<T: Encodable>(_ value: T, forKey defaultName: String) throws {
        try set(JSONEncoder().encode(value), forKey: defaultName)
    }
}

class DbInsideLevel: Codable {
    let idNum: Int!
    var topicId: Int = 0
    var tryCount: Int = 0
    var score: Int = 0
    var isOpen: Bool = false
    var lastPlayedDate: Date?
    init(idNum: Int, topicId: Int, tryCount: Int = 0, score: Int = 0, open: Bool, lastPlayedDate: Date?) {
        self.idNum = idNum
        self.topicId = topicId
        self.tryCount = tryCount
        self.score = score
        self.isOpen = open
        self.lastPlayedDate = lastPlayedDate
    }
    func update(score: Int) {
        if score > self.score {
            self.score = score
        }
        self.tryCount = self.tryCount + 1
    }
}

Playground testing

let insideLevel = DbInsideLevel(idNum: 1, topicId: 2, tryCount: 3, score: 4, open: true, lastPlayedDate: Date())
do {
    try UserDefaults.standard.encode(insideLevel, forKey: "insideLevel")
    let decodedLevel = try UserDefaults.standard.decode(DbInsideLevel.self, forKey: "insideLevel")
    print("decodedLevel idNum", decodedLevel.idNum) // decodedLevel idNum Optional(1)
} catch {
    print(error)
}

edit/update:

It does work for arrays of Codable types as well:

let insideLevel = DbInsideLevel(idNum: 1, topicId: 2, tryCount: 3, score: 4, open: true, lastPlayedDate: Date())
do {
    try UserDefaults.standard.encode([insideLevel], forKey: "insideLevels")
    let decodedLevel = try UserDefaults.standard.decode([DbInsideLevel].self, forKey: "insideLevels")
    print("decodedLevel idNum", decodedLevel.first?.idNum) // decodedLevel idNum Optional(1)
} catch {
    print(error)
}


来源:https://stackoverflow.com/questions/64579000/nskeyedunarchiver-unarchivetoplevelobjectwithdata-return-nil

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!