2014-07-09 6 views
10

Ich habe eine KlasseWie archivieren und entpacken Sie benutzerdefinierte Objekte in Swift? Oder wie Sie benutzerdefinierte Objekte in SWUserDefaults speichern?

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

, und ich will es serialise und Benutzereinstellungen zu speichern.

Zuerst bin ich mir nicht sicher, wie man richtig Kodierer und Decoder schreibt. Also habe ich für init zwei Methoden geschrieben.

Wenn ich versuche, diesen Code auszuführen:

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

App stürzt ab und ich bekomme diese Meldung:

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

Was mache ich falsch?

+2

nur ein Summen zu schicken, vor kurzem habe ich ein Beitrag im Swift Forum beantwortet eine ähnliche Frage, hoffe es hilft. https://devforums.apple.com/message/1044432#1044432 – vladof81

+0

@ Vladof81, das funktioniert perfekt :). – sabiland

Antwort

8

NSKeyedArchiver funktioniert nur mit Objective-C-Klassen, nicht reine Swift-Klassen. Sie können Ihre Klasse mit Objective-C verbinden, indem Sie sie mit dem @objc-Attribut oder durch Erben von einer Objective-C-Klasse wie NSObject kennzeichnen.

Weitere Informationen finden Sie unter Using Swift with Cocoa and Objective-C.

+1

Also, die Frage wäre dann, ist es möglich, Objekte in swift serialisieren? – tadasz

+0

Ja; stellen Sie entweder Ihre Swift-Klasse für Obj-C zur Verfügung, so dass Sie NSKeyedArchiver verwenden können, oder rollen Sie Ihre eigene Serialisierung zu plist oder JSON, wenn Sie pure Swift bleiben wollen. – jatoben

+0

hast du es schon geschafft? Ich habe das gleiche Problem. Es wäre schön, wenn Sie Ihren Quellcode veröffentlichen könnten! – Thomas

11

mit XCode getestet 7.1.1, Swift 2.1 & iOS 9

Sie haben ein paar Optionen, um Ihre (Array von) benutzerdefinierte Objekte zu speichern:

  • NSUserDefaults: to Store App Einstellungen, Voreinstellungen, Benutzereinstellungen:
  • NSKeyedArchiver: für allgemeine Datenspeicherung
  • Daten Kern: für komplexere Datenspeicher (Datenbank wie)

lasse ich Kerndaten aus dieser Diskussion, aber möchte Ihnen zeigen, warum Sie besser NSKeyedArchiver über NSUserDefaults verwenden sollten.

Ich habe Ihre Player-Klasse aktualisiert und Methoden für beide Optionen bereitgestellt. Obwohl beide Optionen funktionieren, wenn Sie die 'load & save' Methoden vergleichen, sehen Sie, dass NSKeydArchiver weniger Code benötigt, um Arrays von benutzerdefinierten Objekten zu behandeln. Auch mit NSKeyedArchiver können Sie einfach Dinge in separaten Dateien speichern, anstatt sich um eindeutige "Schlüssel" -Namen für jede Eigenschaft kümmern zu müssen.

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

diese Klasse I getestet haben, wie (single view app, in dem viewDidLoad Verfahren des Viewcontroller) folgt

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

wie oben gesagt, erzeugen beide Verfahren das gleiche Ergebnis

Auch In einer realen Anwendung könnten Sie eine bessere Fehlerbehandlung in der Anwendung durchführen, da die Archivierung & fehlschlagen kann.

3

In Swift 4 benötigen Sie kein NSCoding mehr! Es gibt ein neues Protokoll namens Codable!

class Player: Codable { 

    var name = "" 

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

Und Codable unterstützt auch Aufzählungen und Structs, so dass Sie Ihre Spieler-Klasse auf eine Struktur neu zu schreiben, wenn Sie wollen!

struct Player: Codable { 
    let name: String 
} 

Um die Spieler in Userdefaults zu sparen:

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

Hinweis: PropertyListEncoder() ist eine Klasse aus dem Rahmen Foundation

Abrufen:

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

Weitere Informationen , lesen Sie https://developer.apple.com/documentation/swift/codable

+0

Dies ist nicht wahr. Nur weil Sie eine Klasse Codable deklarieren, wird es nicht magisch mit NSCoder arbeiten. Es wird mit NSCoder funktionieren, aber Sie müssen es verschlüsseln und dekodieren, und Ihre Antwort erwähnt das nicht oder erklärt wie. – matt

+0

Wo wird NSCoder erwähnt? Ich spreche von Codable ersetzt NSCoding. Wenn Sie wissen wollen, wie man codiert/entschlüsselt, weil es eine Menge Informationen ist, habe ich einen Link mit mehr Informationen zur Verfügung gestellt. –

+0

Es ist der Fehler zu verstehen, wie NSCoder beteiligt ist, dass das Problem ist. Ihre Antwort zeigte nicht, wie der Player in UserDefaults oder auf NSArchiver gespeichert wird. Weil du nicht weißt wie? Wenn Sie wissen, bearbeiten Sie Ihre Antwort und zeigen Sie, wie! – matt

2

Ich habe eine Klasse

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

, und ich will es serialise und Benutzereinstellungen zu speichern.

In Swift 4/iOS 11 gibt es eine völlig neue Möglichkeit, dies zu tun. Es hat den Vorteil, beliebig Swift-Objekt kann es verwenden - nicht nur Klassen, sondern auch Strukturen und enums.

Sie werden feststellen, dass ich Ihre NSCoding-bezogenen Methoden weggelassen habe, da Sie sie für diesen Zweck nicht benötigen. Sie können NSCoding hier übernehmen, wie Sie wissen; aber du musst nicht. (Und eine Struktur oder ENUM kann nicht NSCoding nehmen überhaupt.)

Sie beginnen, indem Sie Ihre Klasse als Annahme des Kodierbare Protokoll erklärt:

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

Es wird dann eine einfache Sache, es in ein Datenobjekt serialisiert werden (NSData), die in UserDefaults gespeichert werden können. Die sehr einfachste Weg ist, eine Eigenschaftsliste als Vermittler zu verwenden:

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

Wenn Sie diesen Ansatz verwenden, lassen Sie uns nun beweisen, dass Sie die gleichen Spieler aus UserDefaults ziehen zurück:

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

Wenn Sie würden lieber einen NSKeyedArchiver/NSKeyedUnarchiver durchlaufen, Sie können dies stattdessen tun. In der Tat gibt es einige Situationen, in denen Sie dies tun müssen: Ihnen wird ein NSCoder angezeigt, und Sie müssen Ihr codierbares Objekt darin codieren. In einer aktuellen Beta-Version hat Xcode 9 einen Weg gefunden, dies auch zu tun. Wenn Sie beispielsweise codieren, werfen Sie den NSCoder in einen NSKeyedArchiver und rufen Sie encodeEncodable.

0

Mit codierbarem neuen ios 11 Protokoll können Sie nun lassen Sie Ihre Klasse implementiert sie und Archiv/unarchive Objekte mit JSONEncoder und 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) 
} 
+0

Es gibt nirgends eine Erwähnung, dass Autor mit JSON befasst –