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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/24659609/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-31 01:18:26  来源:igfitidea点击:

How to archive and unarchive custom objects in Swift? Or how to save custom object to NSUserDefaults in Swift?

ioscocoa-touchswiftios8

提问by tadasz

I have a class

我有一堂课

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
    }
}

and i want to serialise it and save to user defaults.

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

First of all I'm not sure how to correctly write encoder and decoder. So for init i wrote two methods.

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

When i try to execute this code:

当我尝试执行此代码时:

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

app crashes and i get this message:

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

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

What do i do wrong?

我做错了什么?

采纳答案by jatoben

NSKeyedArchiverwill only work with Objective-C classes, not pure Swift classes. You can bridge your class to Objective-C by marking it with the @objcattribute or by inheriting from an Objective-C class such as NSObject.

NSKeyedArchiver仅适用于 Objective-C 类,而不适用于纯 Swift 类。您可以通过使用@objc属性标记您的类或通过从 Objective-C 类(例如NSObject.

See Using Swift with Cocoa and Objective-Cfor more information.

有关更多信息,请参阅将 Swift 与 Cocoa 和 Objective-C 结合使用。

回答by Jeroen Bakker

In Swift 4 you don't need NSCoding anymore! There is a new protocol called Codable!

在 Swift 4 中你不再需要 NSCoding 了!有一个新协议叫做Codable

class Player: Codable {

    var name = ""

    init(name: String) {
        self.name = name
    }
}

And Codablealso supports Enums and Structs so you can rewrite your player class to a struct if you want!

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

struct Player: Codable {
    let name: String
}

To save your player in Userdefaults:

要将您的播放器保存在 Userdefaults 中:

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

Note: PropertyListEncoder() is a class from the framework Foundation

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

To Retrieve:

检索:

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

For more information, read https://developer.apple.com/documentation/swift/codable

有关更多信息,请阅读https://developer.apple.com/documentation/swift/codable

回答by Ronny Webers

tested with XCode 7.1.1, Swift 2.1& iOS 9

使用 XCode 7.1.1、Swift 2.1和 iOS 9进行测试

You have a few options to save your (array of) custom objects :

您有几个选项可以保存您的(数组)自定义对象:

  • NSUserDefaults: to store app settings, preferences, user defaults :-)
  • NSKeyedArchiver: for general data storage
  • Core data : for more complex data storage (database like)
  • NSUserDefaults:存储应用程序设置、首选项、用户默认值 :-)
  • NSKeyedArchiver:用于一般数据存储
  • 核心数据:用于更复杂的数据存储(类似数据库)

I leave Core data out of this discussion, but want to show you why you should better use NSKeyedArchiver over NSUserdefaults.

我把核心数据排除在这个讨论之外,但想告诉你为什么你应该更好地使用 NSKeyedArchiver 而不是 NSUserdefaults。

I've updated your Player class and provided methods for both options. Although both options work, if you compare the 'load & save' methods you'll see that NSKeydArchiver requires less code to handle arrays of custom objects. Also with NSKeyedArchiver you can easily store things into separate files, rather than needing to worry about unique 'key' names for each property.

我已经更新了您的 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]
    }
}

I've tested this class as follows (single view app, in the viewDidLoad method of the ViewController)

我已经按如下方式测试了这个类(单视图应用程序,在 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.
    }
}

as said above, both methods produce the same result

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

Also in a real life application you could do better error handling in the, as archiving & unarchiving could fail.

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

回答by matt

I have a class

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

and i want to serialise it and save to user defaults.

我有一堂课

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

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

In Swift 4 / iOS 11, there's a whole new way to do this. It has the advantage that anySwift object can use it — not just classes, but also structs and enums.

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

You'll notice that I've omitted your NSCoding-related methods, because you won't need them for this purpose. You canadopt NSCoding here, as you know; but you don't have to. (And a struct or enum cannot adopt NSCoding at all.)

您会注意到我省略了与 NSCoding 相关的方法,因为您不需要它们用于此目的。如您所知,您可以在此处采用 NSCoding;但你不必这样做。(并且 struct 或 enum 根本不能采用 NSCoding。)

You start by declaring your class as adopting the Codable protocol:

您首先将您的类声明为采用 Codable 协议:

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

It then becomes a simple matter to serialize it into a Data object (NSData) which can be stored in UserDefaults. The very simplest way is to use a property list as an intermediary:

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

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

If you use that approach, let's now prove that you can pull the same Player back out of UserDefaults:

如果您使用这种方法,现在让我们证明您可以从 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"
    }
}

If you'd rather pass through an NSKeyedArchiver / NSKeyedUnarchiver, you can do that instead. Indeed, there are some situations where you'll have to do so: you'll be presented with an NSCoder, and you'll need to encode your Codable object inside it. In a recent beta, Xcode 9 introduced a way to do that too. For example, if you're encoding, you cast the NSCoder down to an NSKeyedArchiver and call encodeEncodable.

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

回答by sheko

With codablenew ios 11 protocol you can now let your class implements it and archive/unarchive objects of it with JSONEncoderand JSONDecoder

使用可编码的新 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)
}