繁体   English   中英

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

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

我上课了

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 4中,你不再需要NSCoding了! 有一个名为Codable的新协议!

class Player: Codable {

    var name = ""

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

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

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

使用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]
    }
}

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

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

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

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

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

我上课了

  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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM