How to archive and unarchive custom objects in Swift? Or how to save custom object to NSUserDefaults in Swift?
Asked Answered
J

5

20

I have a class

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

and i want to serialise it and save to user defaults.

First of all I'm not sure how to correctly write encoder and decoder. So for init i wrote two methods.

When i try to execute this code:

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

app crashes and i get this message:

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

What do i do wrong?

Javierjavler answered 9/7, 2014 at 16:58 Comment(1)
Just to send a buzz, recently I made a post on Swift forum answering a similar question, hope it helps. devforums.apple.com/message/1044432#1044432Studbook
A
14

NSKeyedArchiver will only work with Objective-C classes, not pure Swift classes. You can bridge your class to Objective-C by marking it with the @objc attribute or by inheriting from an Objective-C class such as NSObject.

See Using Swift with Cocoa and Objective-C for more information.

Affront answered 9/7, 2014 at 21:48 Comment(5)
so, the question would be then, is it possible to serialise objects in swift?Javierjavler
Yes; either make your Swift class available to Obj-C so you can use NSKeyedArchiver, or roll your own serialization to plist or JSON if you want to stay pure Swift.Affront
did you already manage it? I have the same problem. It would be nice if you could post your working source code!Gaultiero
Thanks! I couldn't solve a crash when installing my app new version in Swift over the old version in Obj-C. I had to bridge each class that was used in the archive with the @objc(oldClassName) attribute. Also, I needed to use the OLD Obj-C class names in the @objc prefix, not the new Swift ones.Muddleheaded
"NSKeyedArchiver will only work with Objective-C classes, not pure Swift classes" In Swift 4 iOS 11 that ceases to be true.Wishful
M
18

In Swift 4 you don't need NSCoding anymore! There is a new protocol called Codable!

class Player: Codable {

    var name = ""

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

And Codable also supports Enums and Structs so you can rewrite your player class to a struct if you want!

struct Player: Codable {
    let name: String
}

To save your player in Userdefaults:

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

Note: PropertyListEncoder() is a class from the framework Foundation

To Retrieve:

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

For more information, read https://developer.apple.com/documentation/swift/codable

Moniz answered 7/6, 2017 at 15:15 Comment(8)
This is not true. Just because you declare a class Codable won't make it magically work with NSCoder. It will work with NSCoder, but you have to encode and decode it, and your answer doesn't mention that or explain how.Wishful
Where is NSCoder mentioned? I'm talking about Codable replacing NSCoding. If you want to know how to encode/decode, because it's a lot of information, I've provided a link with more information.Moniz
It is the failure to understand how NSCoder is involved that is the problem. Your answer did not show how to save the Player into UserDefaults or onto NSArchiver. Because you don't know how? If you know, edit your answer and show how!Wishful
See updated answer how to save/retrieve a player in UserDefaults.Moniz
Very good, but now you're just repeating what I said in my answer. I gave you a chance to make this change, and you didn't take it. So I gave an answer. At this point you're just plagiarizing from me.Wishful
Nice tip regarding Codable but doesn't answer the original questionLigni
Why PropertyListEncoder PropertyListDecoder. There is no property list involved. How to do it using Codable protocol but not using json or property list encode and decodeWoodsman
Apple only provides us with JSON or PropertyList Encoders and Decoders. If you want something else, you need to write it yourself.Moniz
A
14

NSKeyedArchiver will only work with Objective-C classes, not pure Swift classes. You can bridge your class to Objective-C by marking it with the @objc attribute or by inheriting from an Objective-C class such as NSObject.

See Using Swift with Cocoa and Objective-C for more information.

Affront answered 9/7, 2014 at 21:48 Comment(5)
so, the question would be then, is it possible to serialise objects in swift?Javierjavler
Yes; either make your Swift class available to Obj-C so you can use NSKeyedArchiver, or roll your own serialization to plist or JSON if you want to stay pure Swift.Affront
did you already manage it? I have the same problem. It would be nice if you could post your working source code!Gaultiero
Thanks! I couldn't solve a crash when installing my app new version in Swift over the old version in Obj-C. I had to bridge each class that was used in the archive with the @objc(oldClassName) attribute. Also, I needed to use the OLD Obj-C class names in the @objc prefix, not the new Swift ones.Muddleheaded
"NSKeyedArchiver will only work with Objective-C classes, not pure Swift classes" In Swift 4 iOS 11 that ceases to be true.Wishful
R
14

tested with XCode 7.1.1, Swift 2.1 & iOS 9

You have a few options to save your (array of) custom objects :

  • NSUserDefaults : to store app settings, preferences, user defaults :-)
  • NSKeyedArchiver : for general data storage
  • Core data : for more complex data storage (database like)

I leave Core data out of this discussion, but want to show you why you should better use NSKeyedArchiver over NSUserdefaults.

I've updated your Player class and provided methods for both options. Although both options work, if you compare the 'load & save' methods you'll see that NSKeydArchiver requires less code to handle arrays of custom objects. Also with NSKeyedArchiver you can easily store things into separate files, rather than needing to worry about unique 'key' names for each property.

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

I've tested this class as follows (single view app, in the viewDidLoad method of the 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.
    }
}

as said above, both methods produce the same result

Also in a real life application you could do better error handling in the, as archiving & unarchiving could fail.

Resource answered 10/1, 2016 at 19:49 Comment(0)
W
7

I have a class

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

and i want to serialise it and save to user defaults.

In Swift 4 / iOS 11, there's a whole new way to do this. It has the advantage that any Swift object can use it — not just classes, but also structs and enums.

You'll notice that I've omitted your NSCoding-related methods, because you won't need them for this purpose. You can adopt NSCoding here, as you know; but you don't have to. (And a struct or enum cannot adopt NSCoding at all.)

You start by declaring your class as adopting the Codable protocol:

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

It then becomes a simple matter to serialize it into a Data object (NSData) which can be stored in UserDefaults. The very simplest way is to use a property list as an intermediary:

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

If you use that approach, let's now prove that you can pull the same Player back out of 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"
    }
}

If you'd rather pass through an NSKeyedArchiver / NSKeyedUnarchiver, you can do that instead. Indeed, there are some situations where you'll have to do so: you'll be presented with an NSCoder, and you'll need to encode your Codable object inside it. In a recent beta, Xcode 9 introduced a way to do that too. For example, if you're encoding, you cast the NSCoder down to an NSKeyedArchiver and call encodeEncodable.

Wishful answered 28/8, 2017 at 14:54 Comment(4)
Why PropertyListEncoder PropertyListDecoder. There is no property list involved,Woodsman
@Alok There is now! You have to encode somehow after all. And in fact NSKeyedArchiver makes a property list too.Wishful
Thanks for this :)Giustino
@Wishful Here I am using Codable plus JSONEncoder/Decoder. I am still stuck with having to use a NSKeyedArchiver method though eg. NSKeyedArchiver.setClassName("MyObject", for: MyObject.self) then let messageData = try? JSONEncoder().encode(myObject). Is there a way to eliminate the use of NSKeyed?Ricardo
G
6

With codable new ios 11 protocol you can now let your class implements it and archive/unarchive objects of it with JSONEncoder and 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)
}
Gabo answered 25/9, 2017 at 11:26 Comment(3)
There is nowhere a mention that author deals with JSONLigni
why JSONEncoder and JSONDecoder. There is no json involvedWoodsman
JSON as a format can be easily stored to UserDefaults.Lymanlymann

© 2022 - 2024 — McMap. All rights reserved.