My Take on Model Layer in Swift
Let’s try to take make our lives a little bit easier by preventing unexpected network (and other) data infecting our software.
1. No Messed Up Entities
I’ve experienced way to many weird bugs and crashes durign my career caused by entities missing a required property, or a property having unexpected type (NSString vs. NSNumber oh my). I want to my property to initialize only if it’s 100% valid.
let json = ["id": "123", "text": "hi"]
do {
let message = MessageEntity(json: json) {
// ...
} catch {
// Report unexpected input data format
}2. Can’t Touch This
I really want to be able to make certain entities immutable to avoid that someone or something would mutate an entity in returned collection and such. These are situations where I can’t even trust myself.
let message = ...
message.text = "hello" // Error, you cannot mess with this instance.
var editedMessage = message
editedMessage.text = "hello" // OK
editedMessage.pushUpdates() { result in
// ...
}
3. Identify Yourself
I want to be able to easily find out what’s the content of given entity, printing out it’s pointer address usually isn’t very helpful.
print("\(message)") // Prints: ["id": "123", "text": "hi"]4. No Black Magic
I like my code to follow the KISS principe (keep it simple stupid).
Let’s do this
Requirement 2 makes us create our entities as structs instead of classes. The issue with structs though is that swift does not support inheritance of structs. This smells like we’d have to implement requirement 3 again and again for each and every entity we create, but there fortunatelly is a way out of this.
Swift struct do not support inheritance, but a struct can conform to a protocol and a protocol can conform to other protocols.
protocol BaseEntity: CustomStringConvertible, CustomDebugStringConvertible {}
Let’s make use of some protocol-oriented programming and implement methods required by CustomStringConvertible and CustomDebugStringConvertible using protocol extension.
extension BaseEntity {
var debugDescription: String {
get {
var dictionary = Dictionary<String, String>()
let mirror = Mirror(reflecting: self)
for childMirror in mirror.children {
let value = childMirror.value
dictionary[childMirror.label!] = “\(value)”
}
return dictionary.description
}
} var description: String {
get {
return debugDescription
}
}
}Looks good, let’s create an entity.
struct MessageEntity: BaseEntity {
var id: String
var text: String
}This will actually compile because swift automatically generates init method witch parameters ate matching it’s properties so we can create an instance like this.
MessageEntity(id: “123”, text: “hello”)
Not bad but I’d like to be able to construct the entity by passing a dictionary and eventually many other data sources. We can actually extend the entity with new inititializer using a protocol extension.
extension MessageEntity {
init(json: [String: AnyObject]) throws {
if let id = json[“id”] as? String where id.characters.count > 0 {
self.id = id
} else {
throw EntityError.InvalidInput
}
if let text = json[“text”] as? String where text.characters.count > 0 {
self.text = text
} else {
throw EntityError.InvalidInput
}
}
}What’s awesome here is that Xcode will perform compile time check that the initializer either assigns values to all non-optional properties or throws an exception. This means that there is no way this can intialize a messed up entity, which fulfils my requirement no. 1.