如何在 Swift 中归档和取消归档自定义对象?或者如何在 Swift 中将自定义对象保存到 NSUserDefaults?

Posted

技术标签:

【中文标题】如何在 Swift 中归档和取消归档自定义对象?或者如何在 Swift 中将自定义对象保存到 NSUserDefaults?【英文标题】:How to archive and unarchive custom objects in Swift? Or how to save custom object to NSUserDefaults in Swift? 【发布时间】:2014-07-09 16:58:13 【问题描述】:

我有课

class Player 

    var name = ""

    func encodeWithCoder(encoder: NSCoder) 
        encoder.encodeObject(name)
    

    func initWithCoder(decoder: NSCoder) -> Player 
        self.name = decoder.decodeObjectForKey("name") as String
        return self
    

    init(coder aDecoder: NSCoder!) 
        self.name = aDecoder.decodeObjectForKey("name") as String
    

    init(name: String) 
        self.name = name
    

我想将它序列化并保存到用户默认值。

首先我不确定如何正确编写编码器和解码器。所以对于init我写了两个方法。

当我尝试执行这段代码时:

func saveUserData() 
    let player1 = Player(name: "player1")
    let myEncodedObject = NSKeyedArchiver.archivedDataWithRootObject(player1)
    NSUserDefaults.standardUserDefaults().setObject(myEncodedObject, forKey: "player")

应用程序崩溃,我收到此消息:

*** NSForwarding: warning: object 0xebf0000 of class '_TtC6GameOn6Player' does not implement methodSignatureForSelector: -- trouble ahead

我做错了什么?

【问题讨论】:

只是想发出一个嗡嗡声,最近我在 Swift 论坛上发了一个帖子,回答了一个类似的问题,希望对您有所帮助。 devforums.apple.com/message/1044432#1044432 @vladof81,这很完美:)。 【参考方案1】:

在 Swift 4 中,您不再需要 NSCoding!有一个名为Codable的新协议!

class Player: Codable 

    var name = ""

    init(name: String) 
        self.name = name
    

而且Codable 还支持枚举和结构,因此您可以根据需要将播放器类重写为结构!

struct Player: Codable 
    let name: String

将播放器保存在 Userdefaults 中:

let player = Player(name: "PlayerOne")
try? UserDefaults.standard.set(PropertyListEncoder().encode(player), forKey: "player")

注意:PropertyListEncoder() 是框架 Foundation 中的一个类

检索:

let encoded = UserDefault.standard.object(forKey: "player") as! Data
let storedPlayer = try! PropertyListDecoder().decode(Player.self, from: encoded)

欲了解更多信息,请阅读https://developer.apple.com/documentation/swift/codable

【讨论】:

这不是真的。仅仅因为你声明了一个类 Codable 不会让它神奇地与 NSCoder 一起工作。它将与 NSCoder 一起使用,但您必须对其进行编码和解码,并且您的答案没有提及或解释如何。 哪里提到了NSCoder?我说的是 Codable 取代 NSCoding。如果您想知道如何编码/解码,因为它包含很多信息,我提供了一个包含更多信息的链接。 问题在于未能理解 NSCoder 是如何参与的。您的回答没有显示如何将播放器保存到 UserDefaults 或 NSArchiver 中。因为你不知道怎么做?如果您知道,请编辑您的答案并展示如何操作! 查看更新的答案如何在 UserDefaults 中保存/检索玩家。 很好,但现在你只是在重复我在回答中所说的话。我给了你一个改变的机会,而你没有接受。所以我给出了答案。在这一点上,你只是在抄袭我。【参考方案2】:

NSKeyedArchiver 仅适用于 Objective-C 类,而不适用于纯 Swift 类。您可以通过使用@objc 属性标记您的类或从诸如NSObject 之类的Objective-C 类继承来将您的类桥接到Objective-C。

更多信息请参见Using Swift with Cocoa and Objective-C。

【讨论】:

那么问题来了,是否可以快速序列化对象? 是的;要么让你的 Swift 类对 Obj-C 可用,这样你就可以使用 NSKeyedArchiver,或者如果你想保持纯 Swift,你可以将自己的序列化为 plist 或 JSON。 您已经管理了吗?我也有同样的问题。如果您能发布您的工作源代码,那就太好了! 谢谢!在 Swift 中安装我的应用程序新版本而不是 Obj-C 中的旧版本时,我无法解决崩溃问题。我必须使用 @objc(oldClassName) 属性来桥接存档中使用的每个类。另外,我需要在 @objc 前缀中使用旧的 Obj-C 类名,而不是新的 Swift 类名。 “NSKeyedArchiver 仅适用于 Objective-C 类,而不适用于纯 Swift 类”在 Swift 4 ios 11 中不再适用。【参考方案3】:

通过 XCode 7.1.1、Swift 2.1 和 iOS 9

测试

你有几个选项来保存你的自定义对象(数组):

NSUserDefaults:存储应用设置、偏好、用户默认值 :-) NSKeyedArchiver:用于一般数据存储 核心数据:用于更复杂的数据存储(类似数据库)

我将核心数据排除在讨论之外,但想向您展示为什么您应该更好地使用 NSKeyedArchiver 而不是 NSUserdefaults。

我已经更新了您的 Player 类并为这两个选项提供了方法。虽然这两个选项都有效,但如果您比较“加载和保存”方法,您会发现 NSKeydArchiver 需要更少的代码来处理自定义对象数组。此外,使用 NSKeyedArchiver,您可以轻松地将内容存储到单独的文件中,而无需担心每个属性的唯一“键”名称。

import UIKit
import Foundation

// a custom class like the one that you want to archive needs to conform to NSCoding, so it can encode and decode itself and its properties when it's asked for by the archiver (NSKeydedArchiver or NSUserDefaults)
// because of that, the class also needs to subclass NSObject

class Player: NSObject, NSCoding 

    var name: String = ""

    // designated initializer
    init(name: String) 
        print("designated initializer")
        self.name = name

        super.init()
    

    // MARK: - Conform to NSCoding
    func encodeWithCoder(aCoder: NSCoder) 
        print("encodeWithCoder")
        aCoder.encodeObject(name, forKey: "name")
    

    // since we inherit from NSObject, we're not a final class -> therefore this initializer must be declared as 'required'
    // it also must be declared as a 'convenience' initializer, because we still have a designated initializer as well
    required convenience init?(coder aDecoder: NSCoder) 
        print("decodeWithCoder")
        guard let unarchivedName = aDecoder.decodeObjectForKey("name") as? String
            else 
            return nil
        

        // now (we must) call the designated initializer
        self.init(name: unarchivedName)
    

    // MARK: - Archiving & Unarchiving using NSUserDefaults

    class func savePlayersToUserDefaults(players: [Player]) 
        // first we need to convert our array of custom Player objects to a NSData blob, as NSUserDefaults cannot handle arrays of custom objects. It is limited to NSString, NSNumber, NSDate, NSArray, NSData. There are also some convenience methods like setBool, setInteger, ... but of course no convenience method for a custom object
        // note that NSKeyedArchiver will iterate over the 'players' array. So 'encodeWithCoder' will be called for each object in the array (see the print statements)
        let dataBlob = NSKeyedArchiver.archivedDataWithRootObject(players)

        // now we store the NSData blob in the user defaults
        NSUserDefaults.standardUserDefaults().setObject(dataBlob, forKey: "PlayersInUserDefaults")

        // make sure we save/sync before loading again
        NSUserDefaults.standardUserDefaults().synchronize()
    

    class func loadPlayersFromUserDefaults() -> [Player]? 
        // now do everything in reverse : 
        //
        // - first get the NSData blob back from the user defaults.
        // - then try to convert it to an NSData blob (this is the 'as? NSData' part in the first guard statement)
        // - then use the NSKeydedUnarchiver to decode each custom object in the NSData array. This again will generate a call to 'init?(coder aDecoder)' for each element in the array
        // - and when that succeeded try to convert this [NSData] array to an [Player]
        guard let decodedNSDataBlob = NSUserDefaults.standardUserDefaults().objectForKey("PlayersInUserDefaults") as? NSData,
              let loadedPlayersFromUserDefault = NSKeyedUnarchiver.unarchiveObjectWithData(decodedNSDataBlob) as? [Player]
            else 
                return nil
        

        return loadedPlayersFromUserDefault
    

    // MARK: - Archivig & Unarchiving using a regular file (using NSKeyedUnarchiver)

    private class func getFileURL() -> NSURL 
        // construct a URL for a file named 'Players' in the DocumentDirectory
        let documentsDirectory = NSFileManager().URLsForDirectory((.DocumentDirectory), inDomains: .UserDomainMask).first!
        let archiveURL = documentsDirectory.URLByAppendingPathComponent("Players")

        return archiveURL
    

    class func savePlayersToDisk(players: [Player]) 
        let success = NSKeyedArchiver.archiveRootObject(players, toFile: Player.getFileURL().path!)
        if !success 
            print("failed to save") // you could return the error here to the caller
        
    

    class func loadPlayersFromDisk() -> [Player]? 
        return NSKeyedUnarchiver.unarchiveObjectWithFile(Player.getFileURL().path!) as? [Player]
    

我已经对这个类进行了如下测试(单视图应用,在ViewController的viewDidLoad方法中)

import UIKit

class ViewController: UIViewController 

    override func viewDidLoad() 
        super.viewDidLoad()

        // create some data
        let player1 = Player(name: "John")
        let player2 = Player(name: "Patrick")
        let playersArray = [player1, player2]

        print("--- NSUserDefaults demo ---")
        Player.savePlayersToUserDefaults(playersArray)
        if let retreivedPlayers = Player.loadPlayersFromUserDefaults() 
            print("loaded \(retreivedPlayers.count) players from NSUserDefaults")
            print("\(retreivedPlayers[0].name)")
            print("\(retreivedPlayers[1].name)")
         else 
            print("failed")
        

        print("--- file demo ---")
        Player.savePlayersToDisk(playersArray)
        if let retreivedPlayers = Player.loadPlayersFromDisk() 
            print("loaded \(retreivedPlayers.count) players from disk")
            print("\(retreivedPlayers[0].name)")
            print("\(retreivedPlayers[1].name)")
         else 
            print("failed")
        
    

    override func didReceiveMemoryWarning() 
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    

如上所述,两种方法产生相同的结果

此外,在现实生活中的应用程序中,您可以更好地处理错误,因为归档和取消归档可能会失败。

【讨论】:

【参考方案4】:

我有课

    class Player 
        var name = ""
        init(name: String) 
            self.name = name
        
    

我想序列化它并保存到用户默认值。

在 Swift 4 / iOS 11 中,有一种全新的方式可以做到这一点。它的优点是 任何 Swift 对象都可以使用它——不仅是类,还包括结构和枚举。

你会注意到我省略了你的 NSCoding 相关的方法,因为你不需要它们来达到这个目的。如您所知,您可以在这里采用 NSCoding;但你不必。 (而且结构体或枚举根本不能采用 NSCoding。)

首先声明你的类采用 Codable 协议:

class Player : Codable 
    var name = ""
    init(name: String) 
        self.name = name
    

然后将其序列化为可以存储在 UserDefaults 中的 Data 对象 (NSData) 变得很简单。最简单的方法是使用属性列表作为中介:

let player = Player(name:"matt")
try? UserDefaults.standard.set(PropertyListEncoder().encode(player), 
    forKey:"player")

如果您使用这种方法,现在让我们证明您可以从 UserDefaults 中拉回同一个 Player:

if let data = UserDefaults.standard.object(forKey:"player") as? Data 
    if let p = try? PropertyListDecoder().decode(Player.self, from: data) 
        print(p.name) // "matt"
    

如果您希望通过 NSKeyedArchiver / NSKeyedUnarchiver,您可以这样做。实际上,在某些情况下您必须这样做:您将看到一个 NSCoder,并且您需要在其中编码您的 Codable 对象。在最近的测试版中,Xcode 9 也引入了一种方法来做到这一点。例如,如果您正在编码,则将 NSCoder 转换为 NSKeyedArchiver 并调用 encodeEncodable

【讨论】:

为什么要使用 PropertyListEncoder PropertyListDecoder。不涉及财产清单, @Alok 现在有!毕竟,您必须以某种方式进行编码。事实上 NSKeyedArchiver 也制作了一个属性列表。 谢谢你:) @matt Here 我正在使用 Codable 和 JSONEncoder/Decoder。我仍然坚持必须使用 NSKeyedArchiver 方法,例如。 NSKeyedArchiver.setClassName("MyObject", for: MyObject.self) 然后let messageData = try? JSONEncoder().encode(myObject)。有没有办法消除 NSKeyed 的使用?【参考方案5】:

使用 codable 新的 ios 11 协议,您现在可以让您的类实现它并使用 JSONEncoderJSONDecoder

struct Language: Codable 
    var name: String
    var version: Int


let swift = Language(name: "Swift", version: 4)
let encoder = JSONEncoder()
if let encoded = try? encoder.encode(swift) 
    // save `encoded` somewhere


if let encoded = try? encoder.encode(swift) 
if let json = String(data: encoded, encoding: .utf8) 
    print(json)


let decoder = JSONDecoder()
if let decoded = try? decoder.decode(Language.self, from: encoded) 
    print(decoded.name)

【讨论】:

没有一处提到作者处理 JSON 为什么选择 JSONEncoder 和 JSONDecoder。不涉及json JSON 作为一种格式可以轻松存储到 UserDefaults。

以上是关于如何在 Swift 中归档和取消归档自定义对象?或者如何在 Swift 中将自定义对象保存到 NSUserDefaults?的主要内容,如果未能解决你的问题,请参考以下文章

在 Swift 中归档自定义对象 - 扩展

在 Swift 中的 DocumentDirectory 中归档和取消归档数组

从 iCloud 文件中取消归档对象时出错

如何将自定义对象的 NSArray 归档到 Objective-C 中的文件

存档和取消存档自定义类(有一个属性作为对象数组),总是失败

在目标 C 中归档多个对象并将它们作为对象数组取消归档