Structuring model data in Swift
Establishing a solid structure within a code base is often essential in order to make it easier to work with. However, achieving a structure that’s both rigid enough to prevent bugs and problems — and flexible enough for existing features and any future changes we’ll want to make down the line — can be really tricky.
That tends to be especially true for model code, which is often used by many different features, that each have their own set of requirements. This week, let’s take a look at a few different techniques for structuring the data that makes up our core models, and how improving that structure can have a big positive impact on the rest of our code base.
Forming hierarchies
At the start of a project, models can often be kept quite simple. Since we haven’t implemented many features yet, our models are most likely not required to contain much data. However, as our code base grows, so often does our models — and it’s quite common to reach a point where a once simple model ends up becoming a “catch-all” for all kinds of related data.
For example, let’s say that we’re building an email client, which uses a Message
model to keep track of each message. Initially, that model might’ve only contained the subject line and body for a given message, but has since then grown to contain all sorts of additional data:
struct Message {
var subject: String
var body: String
let date: Date
var tags: [Tag]
var replySent: Bool
let senderName: String
let senderImage: UIImage?
let senderAddress: String
}
While all of the above data is required in order to render a message, keeping it all directly within the Message
type itself has made things become a bit messy — and will most likely make messages harder to work with, especially when we’re creating new instances — either when composing a new message, or when writing unit tests.
One way to mitigate the above problem would be to instead break our data up into multiple, dedicated types — that we can then use to form a model hierarchy. For example, we might extract all the data about the sender of a message into a Person
struct, and all metadata — such as the message’s tags and date — into a Metadata
type, like this:
struct Person {
var name: String
var image: UIImage?
var address: String
}
extension Message {
struct Metadata {
let date: Date
var tags: [Tag]
var replySent: Bool
}
}
Now, with the above in place, we can give our Message
type a much clearer structure — since each piece of data that’s not directly a part of the message itself is now wrapped inside a more contextual, dedicated type:
struct Message {
var subject: String
var body: String
var metadata: Metadata
let sender: Person
}
Another benefit of the above approach is that we can now much more easily reuse parts of our data in different contexts. For example, we could use our new Person
type to implement features like a contact list, or to enable the user to define groups — since that data is no longer bound directly to the Message
type.
Reducing duplication
Besides being used to better organize our code, a solid structure can also help reduce duplication across a project. Let’s say that our email app uses an event-driven approach to handling different user actions — using an Event
enum that looks like this:
enum Event {
case add(Message)
case update(Message)
case delete(Message)
case move(Message, to: Folder)
}
Using an enum to define a finite list of events that various pieces of code need to handle can be a great way to establish more clear data flows within an app — but our current implementation requires each case to contain the Message
that the event is for — leading to duplication both within the Event
type itself, but also when we want to extract information from an event’s message.
Since each event’s action is performed on a message, let’s instead separate the two, and create a much simpler enum type that’ll contain all of our actions:
enum Action {
case add
case update
case delete
case move(to: Folder)
}
Then, let’s again form a hierarchy — this time by refactoring our Event
type to become a wrapper containing both an Action
, and the Message
that it’ll be applied to — like this:
struct Event {
let message: Message
let action: Action
}
The above approach sort of gives us the best of both worlds — handling an event is now simply a matter of switching on the event’s Action
, and extracting data from an event’s message can now be done directly using the message
property.
Recursive structures
So far we’ve formed hierarchies in which each child and parent have been of completely separate types — but that’s not always the most elegant, or most convenient, solution. Let’s say that we’re working on an app that displays various kinds of content, such as text and images, and that we’re once again using an enum to define each piece of content — like this:
enum Content {
case text(String)
case image(UIImage)
case video(Video)
}
Now let’s say that we want to enable our users to form groups of content — for example by creating a list of favorites, or to organize things using folders. An initial idea might be to go for a dedicated Group
type, that contains the name of the group and the content that belongs to it:
struct Group {
var name: String
var content: [Content]
}
However, while the above looks elegant and well-structured, it has some downsides in this case. By introducing a new, dedicated type, we’ll be required to handle groups separately from individual pieces of content — making it harder to build things like lists — and we also won’t be able to easily support nested groups.
Since a group is nothing more than a different way of structuring content in this case, let’s instead make it a first-class member of the Content
enum itself, by simply adding a new case for it — like this:
enum Content {
case text(String)
case image(UIImage)
case video(Video)
case group(name: String, content: [Content])
}
What we’ve essentially done above is to make Content
a recursive data structure. The beauty of that approach is that we can now reuse much of the same code that we’re using to handle content to handle groups as well, and we can automatically support any number of nested groups.
For example, here’s how we might handle cell selection for a table view displaying a list of content:
extension ListViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let content = contentList[indexPath.row]
switch content {
case .text(let string):
navigator.showText(string)
case .image(let image):
navigator.showImage(image)
case .video(let video):
navigator.openPlayer(for: video)
case .group(let name, let content):
navigator.openList(withTitle: name, content: content)
}
}
}
Above we’re using the Navigator pattern to navigate to new destinations. You can find more info on that in “Navigation in Swift”.
Since Content
is now recursive, calling navigator.openList
when handling a group will now simply create a new instance of ListViewController
with that group’s list of content, enabling the user to freely create and navigate any content hierarchy with very little effort on our part.
Specialized models
While being able to reuse code is often a good thing, sometimes it’s better to create a more specialized new version of a model, rather than attempting to reuse it in a very different context.
Going back to our email app example from before, let’s say that we want to enable users to save drafts of partially composed messages. Rather than having that feature deal with complete Message
instances, which requires data that won’t be available for drafts — such as the name of the sender, or the date the message was received — let’s instead create a much simpler Draft
type, that we’ll nest inside Message
for additional context:
extension Message {
struct Draft {
var subject: String?
var body: String?
var recipients: [Person]
}
}
That way, we’re free to keep certain properties as optionals, and reduce the amount of data that we’ll need to work with when loading and saving drafts — without impacting any of our code that deals with proper messages.
Conclusion
While what kind of model structure that will be the most appropriate for each situation will heavily depend on what kind of data is required, and how that data will be used — striking a balance between being able to reuse code, and not creating models that are too complex, is often key.
Forming clear hierarchies — either with dedicated types or by creating recursive data structures — while still occasionally creating specialized versions of our models for specific use cases, can go a long way to form a much clearer structure within our model code — and like always, constant refactors and small improvements is usually the way to get there.
What do you think? Do you currently use some of the techniques from this article when structuring your model code, or do you have any other favorite ways of doing so? Let me know — along with your questions, comments, or feedback — either by contacting me, or on Twitter @johnsundell.
Thanks for reading! 🚀