Как архивировать и разархивировать пользовательские объекты в Swift? Или как сохранить пользовательский объект в NSUserDefaults в 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

что я делаю не так?

5 ответов


NSKeyedArchiver будет работать только с классами Objective-C, а не с чистыми классами Swift. Вы можете связать свой класс с Objective-C, пометив его @objc атрибут или путем наследования от класса Objective-C, такого как NSObject.

посмотреть использование Swift с какао и Objective-C для получения дополнительной информации.


протестировано с помощью XCode 7.1.1,Swift 2.1 & iOS 9

у вас есть несколько вариантов для сохранения ваших (массив) пользовательских объектов:

  • NSUserDefaults : для хранения настроек приложения, Настройки пользователя по умолчанию :-)
  • NSKeyedArchiver : для общего хранения данных
  • основные данные : для более сложного хранения данных (например, базы данных)

Я оставляю основные данные из этого обсуждения, но хочу покажите, почему вам лучше использовать NSKeyedArchiver над NSUserdefaults.

я обновил ваш класс игрока и предоставил методы для обоих вариантов. Хотя оба варианта работают, если вы сравните методы "load & save", вы увидите, что 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]
    }
}

я протестировал этот класс следующим образом (приложение single view, в методе viewDidLoad ViewController)

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

Как сказано выше, оба метода дают один и тот же результат

также в реальной жизни вы могли бы сделать лучше обработки ошибок, как Архивация и распаковка может провалиться.


в 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 () является классом из framework 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


у меня есть класс

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

и я хочу сериализовать его и сохранить для пользователя по умолчанию.

в Swift 4 / iOS 11 есть совершенно новый способ сделать это. Это имеет то преимущество, что любой Swift объект может использовать его-не только классы, но и структуры и перечисления.

вы заметите, что я опустил ваши методы, связанные с NSCoding, потому что они вам не понадобятся для этой цели. Вы can принять NSCoding здесь, как вы знаете, но вам и не нужно. (И структура или перечисление не могут принять NSCoding вообще.)

вы начинаете с объявления вашего класса как принятие кодируемого протокола:

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

затем становится простым делом сериализовать его в объект данных (NSData), который может быть сохранен в UserDefaults. Самый простой способ - использовать список свойств в качестве посредника:

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

если вы используете этот подход, давайте теперь докажем, что вы можете вытащить того же игрока из 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, и вам нужно будет кодировать ваш кодируемый объект внутри него. В недавней бета-версии 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)
}