This week, I’d like to examine building your entire model layer out of Swift structs. Swift allows you to build your objects in two ways: values types and references types.
Reference types behave more like the objects we’re used to. They’re created with the class
keyword, and they are pass-by-reference. This means that multiple other pieces of code could have a handle on the same object, which, when combined with mutable properties, can lead to issues of thread safety and data in an inconsistent state.
Value types, on the other hand, are pass-by-value. They’re created with the struct
keyword. Passing things by value means that, in practice, two pieces of code can’t mutate the same struct at the same time. They’re commonly used for types that are mostly a bag-of-data.
In Objective-C, you didn’t have the ability to use value types at all. In Swift, we have a new thing called a struct
which automatically copies itself every time it’s used in a new place. At first blush, this seems like exactly what we want for our model layer. It holds data and can’t be shared between threads, making it much safer. I want to know, can I write my whole model layer out of this?
Drew Crawford wrote a post called “Should I use a Swift struct or a class?”. The general idea is that with the advent of this new tool (structs), a lot of people have promoted writing as much of your code as possible in structs, rather than classes.
This leads to vaguely positive statements like Andy Matuschak’s in which he is “emphatically not suggesting that we build everything out of inert values” and yet we should “Think of objects as a thin, imperative layer” which presumably leaves a thick layer of values for everything else, and “As you make more code inert, your system will become easier to test and change over time” which is vaguely true but when taken to its logical conclusion seems to contradict his earlier statement that “not everything” should be a struct.
Drew’s general recommendation is that when it’s meaningful for the type to conform to Equatable
, it can be a struct. If not, it should be a class. This suggests that our model objects maybe should be represented as a struct. My model objects usually do conform to Equatable
, comparing the values that represent their identity.
Drew also quotes Apple’s book on Swift:
As a general guideline, consider creating a structure when one or more of these conditions apply:
- The structure’s primary purpose is to encapsulate a few relatively simple data values.
This, on the other hand, suggests that structs are too simplistic for the model layer. Models are usually more than “a few relatively simple data values”.
Examples of good candidates for structures include:
- The size of a geometric shape, perhaps encapsulating a width property and a height property, both of type Double.
- A way to refer to ranges within a series, perhaps encapsulating a start property and a length property, both of type Int.
- A point in a 3D coordinate system, perhaps encapsulating x, y and z properties, each of type Double.
These sound like parts of a model, rather than model layer itself.
Because model objects lie in a gray area between “things that need to be alive and responsive” and “things whose central job is to hold data”, I’m surprised to see models-as-structs question hasn’t been asked yet.
Models are primarily value data (like strings and integers) and there are going to be people who try to make their whole model layer out of structs, so I think it’s worth it to examining this approach. After all, the “models” in a purely functional programming language like Haskell has to be pass-by-value. Can’t it be done here?
The answer is, in short, yes. But there are a lot of caveats.
Persistence is much more difficult. You can’t conform to NSCoding
without being an NSObject
. To use NSCoding
with a struct, you have to outsource your object encoding to a reference type, as I lay out in this post. Core Data and Realm are totally not options, since those both have to subclass from NSManagedObject
and Realm’s Object. To use them, you have to define your model twice and painstakingly copy properties from your structs to your persistence objects.
Changes in one model struct won’t be reflected elsewhere. In a lot of cases, changing a property of a object should be reflected in more than one place. For example, faving a tweet in a detail view should be reflected on the timeline as well. If you need behavior like that, be prepared to write a bunch of state maintenence code and notifications for shuttling those changes around to every corner of your app that needs them.
You can’t have circular references. Some people might consider this a pro rather than a con, but structs can’t describe circular relationships. If you want to get all the tags of a post, and then go find all the posts of one of those tags, you’re going to have to either duplicate the data or go through intermediate objects. Your data must be a hierarchal/tree structure, and can’t contain loops. This is what JSON looks like by default, so if your app’s model is a thin layer over a web service, this is less of a downside for you.
Not all types in your model will be representable by value types. Specifically, colors, URLs, data, fonts, and images, even though they act like values, can only be represented by class-based reference types, like UIColor
, NSURL
, and their companions. To make these truly value types, you’ll have to do work either wrapping that class in a struct, or defining a new data structure that represents the data and can be readily converted into a Foundation- and UIKit-friendly type.
If you want to make your model layer out of structs, it’s not impossible, but the downsides can be great. As with the rest of programming, it’s a trade-off that you must weigh to make a decision.