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

tad*_*asz 14 cocoa-touch ios swift ios8

我上课了

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
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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

当我尝试执行此代码时:

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

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

*** NSForwarding: warning: object 0xebf0000 of class '_TtC6GameOn6Player' does not implement methodSignatureForSelector: -- trouble ahead
Run Code Online (Sandbox Code Playgroud)

我做错了什么?

Jer*_*ker 14

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

class Player: Codable {

    var name = ""

    init(name: String) {
        self.name = name
    }
}
Run Code Online (Sandbox Code Playgroud)

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

struct Player: Codable {
    let name: String
}
Run Code Online (Sandbox Code Playgroud)

要在Userdefaults中保存播放器:

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

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

要检索:

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

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

  • 这不是真的。仅仅因为你声明了一个类 Codable 并不能让它神奇地与 NSCoder 一起工作。它可以与 NSCoder 一起使用,但是您必须对其进行编码和解码,并且您的答案没有提到这一点或解释如何进行。 (3认同)

Ron*_*ers 12

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

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

  • NSUserDefaults:存储应用设置,首选项,用户默认值:-)
  • NSKeyedArchiver:用于一般数据存储
  • 核心数据:用于更复杂的数据存储(类似数据库)

我将Core数据排除在讨论之外,但是想要告诉你为什么你应该更好地使用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]
    }
}
Run Code Online (Sandbox Code Playgroud)

我已按如下方式测试此类(单视图应用程序,在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.
    }
}
Run Code Online (Sandbox Code Playgroud)

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

此外,在现实生活中,您可以更好地进行错误处理,因为归档和取消归档可能会失败.


jat*_*ben 11

NSKeyedArchiver只能使用Objective-C类,而不是纯Swift类.您可以通过使用@objc属性标记它或通过继承Objective-C类(如.class)来将类桥接到Objective-C NSObject.

有关更多信息,请参阅将Swift与Cocoa和Objective-C一起使用.

  • "NSKeyedArchiver只适用于Objective-C类,而不是纯粹的Swift类"在Swift 4 iOS 11中不再是真实的. (2认同)

mat*_*att 5

我有一堂课

    class Player {
        var name = ""
        init(name: String) {
            self.name = name
        }
    }
Run Code Online (Sandbox Code Playgroud)

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

在Swift 4 / iOS 11中,有一种全新的方法来执行此操作。它的优点是任何 Swift对象都可以使用它-不仅是类,还可以使用结构和枚举。

您会注意到,我省略了与NSCoding相关的方法,因为您无需为此使用它们。众所周知,您可以在此处采用NSCoding。但您不必。(并且一个结构或枚举根本不能采用NSCoding。)

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

class Player : Codable {
    var name = ""
    init(name: String) {
        self.name = name
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

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


she*_*eko 5

使用可编码的新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)
}
Run Code Online (Sandbox Code Playgroud)

  • 无处提及作者处理JSON (2认同)
  • JSON 作为一种格式可以轻松存储到 UserDefaults。 (2认同)