Clean Xcode project architecture is an onion that can be peeled a million different ways. And one thing most iOS engineers agree is that Model-View-Controller (MVC) is just a small slice of the architectural story.
A problem that comes up for most projects is the challenge of integrating dependencies, whether they be Apple frameworks, third-party SDKs, or simple HTTP APIs.
I'm not talking about dependency management in the vein of Swift Package Manager or CocoaPods. I'm talking about how to organize and integrate the code you write which makes use of those dependencies.
I use what I call an "integrations" pattern to plug in and unplug dependencies as neatly as possible. Here's how it works.
Integrations vs alternatives
This "integrations" pattern is not a complete architectural system. Rather, it’s simply a way to take a hard stance on separating third-party code out from the guts of your app. All third-party code, along with the code you write that uses such code, exists in its own silo. So when you inevitably find yourself swapping one dependency for another, your models and views don't need to be touched.
You can make this pattern work in a variety of architectural systems. It can be used with view models or not, and it's especially useful for patterns that don't already account for third-party dependencies like the umpteenth flavors of MVC you see in existing codebases.
Sure, I'm going to reference architectural concepts that I use to make this work (e.g. how I set up models and link them to my integrations). But I encourage you to take these as guideposts and allow yourself to internalize the high-level concepts which can then be adapted in other ways that suit your needs.
After all, I didn't invent the idea of modularizing dependencies. What I'm proposing here is a flavor I landed on after learning a lot from others in the iOS community. And I like it because of its flexibility and the architectural aesthetics it makes possible.
Models of your hearts desire
The best part of using integrations is getting to do whatever you want with your models. Swift is one of the most readable programming languages ever created. It has all manner of sexy considerations that serve its first and foremost API guideline: clarity at the point of use.
So I damn-well want to use these features. I want to use structs when they make sense. I want to employ generics and extensions to reduce code reuse. I want to use all the nifty features that come with enums. And I want to do this with full freedom, unapologetically.
I want my models to be sexy, to-the-point, simply-named, crystal-clear, fully Swift native representations of my data that get to use the latest and greatest features my deployment target permits.
So that's what I do, as best as I can. I design my models in an app-specific way to my heart's content. And then I defer the responsibility of talking to third party code to the integration implementation.
Services as a bridge
Your models are of course purely a representation of app state. So we need something on top of this layer to fetch and mutate this state. And we want this defined in terms of our app models, and completely (or at least as much as possible) without reference to how it's actually implemented.
And you could implement this service however you like. If you are using a simple HTTP API, it might look like this:
Or you could use something like Firebase:
Integrations are comprehensive
Integrations are intended to be comprehensive. That is, all code you write, of any kind, that directly interacts with a given third-party dependency lives inside the integration.
As such, I like to add an “Integrations” group (folder) along with all my other layers like so:
Then I add a group for each integration, each of which is a little universe unto itself with everything needed for the integration to work. In this case, I integrate the Facebook SDK, the Firebase SDK, the Airtable API for one of my Airtable "bases," and an HTTP API specific to my app:
Notice the Firebase integration. There’s a manager class that handles all initialization and high-level code needed for the Firebase SDK to run (e.g. key setup, calls in response to app lifecycle events, etc). There’s a group for all the app-specific services the integration implements (see above), and there’s also a group for Firebase integration-specific models and view controllers. We’ll get to why you’d ever need these later, but the point is that each integration has a group for each of the types of items needed for that integration to function.
Integrations are for views too
Integrations aren't purely about abstracting away things like persistence frameworks. They are about abstracting any kind of third-party code you might imagine, even views.
For example, Google's Firebase provides a fully fleshed-out authentication flow that you can drop right into your app, which can be a massive time saver for early-stage startups with small teams and limited budgets. It's not gorgeous, but it's fine for an MVP.
Thing is, I'm not interested in seeing Firebase view and view controller classes mixed in with my app specific view code. So integrations come to the rescue again!
So how do I get around this?
But since we're delegating our entire authentication flow to Firebase, it's not unreasonable to ask the service to provide that dependency like so:
Integration-specific models anyone?
Another group/folder I frequently have associated with an integration is a “models” folder. That’s right, I will occasionally decide to define model structs for use solely within an integration — even if a given model is already represented by a model I’ve already defined for the app.
Now that might have you wondering whether I’m actually a sane person. Models are tedious enough to setup as it is. Why on earth would someone ever define a model more than once?
There’s reasons, so hear me out.
Remember, my deepest desire for the models used throughout the app are to be designed in such a way that they have the following characteristics:
- They take full advantage of the latest and greatest Swift language features
- They are designed around “clarity at the point of use” within the app
- They are unaware of the implementation details of the services on which they depend
The problem is that using app-level models in an integration can sometimes violate one or more of these principles, allowing the tentacles of dependencies to reach places they shouldn’t.
But what if your app interfaces with multiple APIs and SDKs that need to know about your models? And what if each of these represents the data a little differently or only interfaces with some slice of each model?
It’s a lot easier, and less error-prone, to reason through the details of mapping the same model data represented in different ways at the level of Swift structs and initializers than it is to do so in the context of the serialization protocols.
Strong modularity ✅
Clarity at the point of use ✅
Fewer errors during serialization and deserialization ✅
No limits on your app-specific models ✅
Happy feelings ✅
Triplebyte helps engineers find great jobs by assessing their abilities, not by relying on the prestige of their resume credentials. Take our 30 minute multiple-choice coding quiz to connect with your next big opportunity and join our community of 200,000+ engineers.