Swift and NSCoding: Keeping it Simple (update Swift 3, Xcode 8 GM)


** Jump to Swift 3 code **

As a beginner, I had no idea what a coder was or how I might use it to initialize a class. But in fact there's nothing whatsoever to be scared of, as is demonstrated in this excellent post by the NSHipster site from 2013 (updated with Swift examples).

Taking that code and adding it to a SwiftFiles Playground (while updating for Swift 1.2 and making a few adaptations)
class Book: NSObject, NSCoding {
    var title: String
    var author: String
    var pageCount: Int
    var categories: [String]
    var available: Bool
    
    init(title:String, author: String, pageCount:Int, categories:[String],available:Bool) {
        self.title = title
        self.author = author
        self.pageCount = pageCount
        self.categories = categories
        self.available = available
    }
    
    // MARK: NSCoding
    required convenience init(coder decoder: NSCoder) {
        
        let title = decoder.decodeObjectForKey("title") as! String
        let author = decoder.decodeObjectForKey("author")as! String
        let categories = decoder.decodeObjectForKey("categories") as! [String]
        let available = decoder.decodeBoolForKey("available")
        let pageCount = decoder.decodeIntegerForKey("pageCount")
        
        self.init(title:title, author:author,pageCount:pageCount,categories: categories,available:available)
    }
    
    func encodeWithCoder(coder: NSCoder) {
        coder.encodeObject(self.title, forKey: "title")
        coder.encodeObject(self.author, forKey: "author")
        coder.encodeInt(Int32(self.pageCount), forKey: "pageCount")
        coder.encodeObject(self.categories, forKey: "categories")
        coder.encodeBool(self.available, forKey: "available")
    }
}
we first of all create a class that can be initialized using an NSCoder instance and encode itself into an instance of NSCoder. And once we have this functionality, which is required by the NSCoding protocol, it is possible to create an instance that can be archived
let book = Book(title: "MyBook", author: "Me", pageCount: 10, categories: ["Fabulousness"], available: true)

let filePath = FileSave.buildPath("bookdata", inDirectory: NSSearchPathDirectory.CachesDirectory, subdirectory: "archive")
NSKeyedArchiver.archiveRootObject(book, toFile: filePath)
and de-archived.
if let bookData = NSKeyedUnarchiver.unarchiveObjectWithFile(filePath) as? Book {
    bookData.available // true
    bookData.author // Me
}
And equally easy to create an array of instances that can be archived and de-archived:
let books = [Book(title: "MyBook", author: "Me", pageCount: 10, categories: ["Fabulousness"], available: true),Book(title: "YourBook", author: "You", pageCount: 10, categories: ["Fabulousness","Creativity"], available: true)]

let filePath = FileSave.buildPath("bookdata", inDirectory: NSSearchPathDirectory.CachesDirectory, subdirectory: "archive")
NSKeyedArchiver.archiveRootObject(books, toFile: filePath)

if let bookData = NSKeyedUnarchiver.unarchiveObjectWithFile(filePath) as? [Book] {
    bookData[1].available // true
    bookData[1].title // YourBook
}
Notice that we never once have to call init(coder:) or encodeWithCoder(), since this happens  automatically when we use NSKeyedArchiver and NSKeyedUnarchiver. 

Conclusion

We see very quickly the utility of NSCoding, and how simply we can save to disk data that we can avoid (or delay) reconstructing from the file-system or from server data. Explained in this way (thanks to NSHipster) it becomes almost as straightforward as using NSUserDefaults but far more powerful for restoring the state of an app.

Swift 3, Xcode 8 GM

The book class rendered in Swift 3 looks like this:
class Book: NSObject, NSCoding {
    var title: String
    var author: String
    var pageCount: Int
    var categories: [String]
    var available: Bool
    
    init(title:String, author: String, pageCount:Int, categories:[String],available:Bool) {
        self.title = title
        self.author = author
        self.pageCount = pageCount
        self.categories = categories
        self.available = available
    }
    
    // MARK: NSCoding
    public convenience required init?(coder aDecoder: NSCoder) {
        
        let title = aDecoder.decodeObject(forKey: "title") as! String
        let author = aDecoder.decodeObject(forKey: "author") as! String
        let categories = aDecoder.decodeObject(forKey: "categories") as! [String]
        let available = aDecoder.decodeBool(forKey: "available")
        let pageCount = aDecoder.decodeInteger(forKey: "pageCount")
        
        self.init(title:title, author:author,pageCount:pageCount,categories: categories,available:available)
    }
    
    func encode(with aCoder: NSCoder) {
        aCoder.encode(title, forKey: "title")
        aCoder.encode(author, forKey: "author")
        aCoder.encodeCInt(Int32(pageCount), forKey: "pageCount")
        aCoder.encode(categories, forKey: "categories")
        aCoder.encode(available, forKey: "available")
    }
}
Full code for all examples can be found on GitHub. This has the SwiftFiles library (updated for Swift 3) included and so saves and loads the data as well.


Comments

  1. Hello! Thanks for the post. I'm getting the message "use of unresolved identifier FileSave"... could you please help me with this?

    ReplyDelete
    Replies
    1. This has now been fixed, you need to update your version of SwiftFiles - https://github.com/sketchytech/SwiftFiles/tree/Swift_2

      Delete

Post a Comment