如何在Swift中存档和取消存档自定义对象? 或者如何将自定义对象保存到Swift中的NSUserDefaults?
我有一堂课
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
我做错了什么?
NSKeyedArchiver
将只能使用Objective-C类,而不是纯Swift类。 您可以通过使用@objc
属性或从Objective-C类(如NSObject
inheritance来将您的类桥接到Objective-C。
有关更多信息,请参阅在Cocoa和Objective-C中使用Swift 。
使用XCode 7.1.1, Swift 2.1和iOS 9进行testing
你有几个选项来保存你的(数组)自定义对象:
- NSUserDefaults :存储应用程序设置,首选项,用户默认设置:-)
- NSKeyedArchiver :用于一般数据存储
- 核心数据:用于更复杂的数据存储(类似数据库)
我把Core数据放在这个讨论之外,但是想告诉你为什么你应该更好地使用NSKeyedArchiver而不是NSUserdefaults。
我已经更新了您的Player类,并为这两个选项提供了方法。 虽然这两个选项都起作用,但如果比较“加载和保存”方法,则会看到NSKeydArchiver需要较less的代码来处理自定义对象的数组 。 同样,使用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] } }
我已经testing这个类如下(单视图应用程序,在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. } }
如上所述,两种方法产生相同的结果
同样在现实生活中,你可以做更好的error handling,因为归档和解除归档可能会失败。
在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()是框架基础的一个类
检索:
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
我有一堂课
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 } }
然后把它序列化成一个Data对象(NSData),它可以存储在UserDefaults中变成一件简单的事情。 最简单的方法是使用财产清单作为中介:
let player = Player(name:"matt") try? UserDefaults.standard.set(PropertyListEncoder().encode(player), forKey:"player")
如果您使用这种方法,现在让我们certificate您可以将相同的播放器从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" } }
如果你想通过NSKeyedArchiver / NSKeyedUnarchiver,你可以这样做。 事实上,在某些情况下,您必须这样做:您将看到一个NSCoder,您需要在其中编码您的Codable对象。 在最近的testing版中,Xcode 9也引入了一个方法来做到这一点。 例如,如果您正在编码,则将NSCoder强制转换为NSKeyedArchiver并调用encodeEncodable
。
通过使用可编译的新ios 11协议,您现在可以让您的类实现它,并使用JSONEncoder和JSONDecoder存档/取消存档对象
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) }