Swift Struct & NSCoding
--
I am currently working with a client highly embraces value types in Swift 3, and leverage struct
as much as possible. This has many advantages by taking the benefits from using value types. 👍
Everyone in this team are on the same page and are happy about this, then during code reviews, I see class Foo: NSObject, NSCoding
many times. Apparently, we want to serialize the models, and try to useNSCoding
, but compiler won’t be happy about this if we use struct
, because Non-class type 'Foo' cannot inherit from class 'NSObject'
. So developers fall back on class
. 💔
I have seen many solutions about this, Apple’s document and some StackOverflow posts simply use class
. Other approaches like adding custom encoder and decoder classes for each model requires a lot of code. Bringing another third-party library is an overkill, especially at this time, Swift changes a lot, and waiting for third-party developers to catch up sometimes introduces unnecessary delay to project development.
The approach I used before was to convert the model to NSDictionary
, and serialize on it. But then I need to write this messy conversion code, especially if I need to deal with types likeInt64
.
After some serious thinking, I took another approach this time with the team, and after using it for a while, the team likes it a lot. My take on it this time is to keep using NSCoding
that developers are used to, but make it into a nested class that is embedded in the struct
. In addition, two tiny protocol
s, namely Encodable
and Decodable
, are introduced to ease the serialization. 🤓
Let’s say we have a model called Event
, which is a struct
, like this:
public struct Event {
public internal(set) var timeStamp: Date
public internal(set) var eventTag: String
public init(timeStamp: Date, tag: String) {
self.timeStamp = timeStamp
self.eventTag = tag
}
}
Simple and straight forward. Now, I want to use NSCoding
for my typical encoding and decoding. Then I make a nested Event.Coding
class inherits from NSObject
and NSCoding
like this:
extension Event {
class Coding: NSObject, NSCoding {
let event: Event?
init(event: Event) {
self.event = event
super.init()
}
required init?(coder aDecoder: NSCoder) {
guard let timeStamp = aDecoder.decodeObject(forKey: "timeStamp") as? Date, let eventTag = aDecoder.decodeObject(forKey: "eventTag") as? String else {
return nil
}
event = Event(timeStamp: timeStamp, tag: eventTag)
super.init()
}
public func encode(with aCoder: NSCoder) {
guard let event = event else {
return
}
aCoder.encode(event.timeStamp, forKey: "timeStamp")
aCoder.encode(event.eventTag, forKey: "eventTag")
}
}
}
So the idea behind this is basically wrapping the struct
model into this Coding
class, and work indirectly on model’s properties rather than the properties of self
as if the model class itself is a subclass of NSCoding
. 👏
This basically does all the serialization work already for the individual model, and just to make it generic, two symmetrical protocol
s can be introduced:
protocol Encodable {
var encoded: Decodable? { get }
}protocol Decodable {
var decoded: Encodable? { get }
}
And then we can conform our Event
model with the protocol
s:
extension Event: Encodable {
var encoded: Decodable? {
return Event.Coding(event: self)
}
}extension Event.Coding: Decodable {
var decoded: Encodable? {
return self.event
}
}
The benefits of introducing Encodable
and Decodable
protocol
s will show up when we deal with not only one single model object but also an array of the objects. As a real-world example, by having the following extension
s, all our models will work very well inside arrays, sets, etc.
extension Sequence where Iterator.Element: Encodable {
var encoded: [Decodable] {
return self.filter({ $0.encoded != nil }).map({ $0.encoded! })
}
}extension Sequence where Iterator.Element: Decodable {
var decoded: [Encodable] {
return self.filter({ $0.decoded != nil }).map({ $0.decoded! })
}
}
There is also code simplicity when we have another model Session
that contains a list of Event
, and we want to serialize on the Session
model as well. For instance, our Session
model can be something like https://gist.github.com/ryuichis/f98f0b725156e15638982458b4a6ba8f#file-session-swift.
To put all of these into use, as an example, we have few sessions and each session has many events. We want to write them as Data
or archive into files, and later on, we want to have our sessions and events back. Our code will be like this:
var session1 = Session(startTime: Date(), endTime: Date())
session1.events.append(Event(timeStamp: Date(), tag: "test session1/event1"))
session1.events.append(Event(timeStamp: Date(), tag: "test session1/event2"))
var session2 = Session(startTime: Date(), endTime: Date())
session2.events.append(Event(timeStamp: Date(), tag: "test session2/event1"))
session2.events.append(Event(timeStamp: Date(), tag: "test session2/event2"))
session2.events.append(Event(timeStamp: Date(), tag: "test session2/event3"))
let sessions: [Session] = [session1, session2]let data = NSKeyedArchiver.archivedData(withRootObject: sessions.encoded)
let back = (NSKeyedUnarchiver.unarchiveObject(with: data) as? [Session.Coding])?.decoded
print(back ?? "error in getting back sessions")/*
[
Session(
startTime: <date>,
endTime: Optional(<date>),
events: [
Event(timeStamp: <date>, eventTag: "test session1/event1"),
Event(timeStamp: <date>, eventTag: "test session1/event2")]),
Session(
startTime: <date>,
endTime: Optional(<date>),
events: [
Event(timeStamp: <date>, eventTag: "test session2/event1"),
Event(timeStamp: <date>, eventTag: "test session2/event2"),
Event(timeStamp: <date>, eventTag: "test session2/event3")])]
*/
🎉
All the examples here can be found in this gist. It is working with Xcode 8.1 beta 3.