The Pragmatic Guide to Scalable Swift Architecture for 2020
How to Transform Disaster iOS Projects into Excellent Swift Design Patterns one Sprint at a Time
I remember the hardest week I’ve ever worked.
Several years ago, I needed to present a core feature of my startup’s app to the CEO & investors.
We had a flimsy plan on making a private photo album where any photo could be shared to a specific group of people.
I wanted to be extra and make it amazing. Because overdelivering.
The problem was I only had a week to do it, and I was using UICollectionView
— tech that Apple had just released and very few apps had adopted.
The meeting was at the end of the week. I hadn’t begun.
Luckily for my investors, and unluckily for my health, I was willing to make up for my lack of skill with 90 hours of work.
So that’s what I did. At a certain point, my brain started bleeding.
That’s what it felt like at least. I got to a point where my cognitive energy ran out and I become some sort of adrenaline monster.
Adrenaline monsters typically write shitty code. No thought — only raw, trial-and-error driven effort. It’s like using a machine gun to do the job of a rifle — you’re gonna miss a lot so you gotta adjust till you hit then cry about how much the recoil hurt your arm.
I got it done… at the expense of my next two weeks of energy.
In the perfect world, we are all architecture purists. We build code written with the latest swanky architectural trends and sniff the air of our own butts in satisfaction.
But the whole real world really fucks that right up, doesn’t it?
We adopt code where:
- some asshat like past me wrote shit code to meet a deadline
- a manager hired some cheap junior developers to launch their first app ever.
- a stressed executive forced his team to push an app out fast to keep up with competitors
And guess what?! You’re blessed to be the person who has to work with scaling a codebase with ViewController
s of over 800 lines and horrifying if else
chains.
Nearly all programmers are thrown into positions where they’re working with architecture they had no control over.
The good thing: Once you embrace the little control you actually have, you’ll be ready to make real architectural progress.
How Top Paid Developers Approach Implementing Architecture Patterns on Teams
Product owners have limited time. Some don’t know how to code.
Even if they do, they have stakeholders who don’t and only speak the language of results.
Architecture changes don’t always have immediate results.
Your app will get faster with less bugs likely, but it’s not a new feature. Many stakeholders want something sparkly with a cute ribbon.
You’re not winning over stakeholders with visuals. Which means you’re going to use pursuasion and tact.
I wish I could just waltz into a stakeholder meeting, show them a beautiful powerpoint of why the app’s entire architecture should be rewritten in Lotus MVC and score consensus agreement.
That won’t happen. From their perspective, they may view your team taking too much time to fulfill very few business interests.
We need to instead win by improving the architecture one small battle at a time.
Here are some ways architecture gets better in reality:
Refactor WHILE working on bugs or within features.
For any task assigned to you, always have awareness if a micro refactor can set your code base up better in the future.
Clean Code calls it leaving the campfire better than you found it (the boyscout way).
For every new feature, you write it correctly from the start.
Start out on the right foot — your codebase wins while also making stakeholders happy.
Don’t ignore your team’s coding standards — if you take certain architectural patterns without considering the team standards (like naming conventions, file organization, and open source choices), you might get more pushback in code review than you’d like.
Start with micro architecture improvements. They’re easier to sell to the team.
Make SMALL tickets for refactoring specific parts of the codebase
When bad architecture is stinking up part of the codebase, the rate of improving the app slows down and a constant supply of bugs appear.
Bugs and slow development are things your steakholders and management will notice. Suggesting architecture changes here becomes WAY easier.
Architectural improvements are the compound interest of coding.
I like to think that for every minute of upfront I work on improving the architecture, I save 10 minutes in the future.
Why I’m Qualified to Write This
I’m Rob Caraway and have been building iOS apps since 2011.
My own apps as an entrepreneur have achieved over 1 Millions Downloads. I’m a top 5% of iOS contributors on Stack Overflow.
I’ve worked as a Senior iOS Developer and iOS Lead on a few billion-dollar companies’ apps: HEB, Oracle’s Moat, Kubota, i-Clicker to name a few.
This article is meant for all levels of developers but best for those who are junior to mid level and want to improve.
I’ve worked with a lot of smart people whove been programming apps since the App Store launched and are a huge reason I’ve picked up some of these patterns.
5 Important Swift Patterns that will Immediately Improve your Apps Scalability
How Making Models Structs with Base Values is Almost Always the Right Decision
Originally argued by iOS Vet Matteo Manferdini and Andy Matuschak, this pattern is slowly becoming the standard on most teams I’ve worked with.
Structs
in Swift are inert value types and can only have one owner. This means they each carry a unique reference to their own data.
This means:
Since the values from photo2
are being written to photo1
, Swift will actually copy them instead of assigning the same memory reference. This is called Copy-On-Write.
When a new value is assigned to id
the same thing happens, so photo1
id value is 39284
but photo2
will remain 98
. So comparing them will return false.
Structs
are also allocated on the stack rather than the heap. This means in many cases, they’ll be several factors faster (confirm it yourself by copying that exercise into a playground).
Imagine using a class
in this instance:
We’ve just assigned the photo
to both the photoManager
and the viewController
.
This can create several issues:
- The
viewController
is now free to manipulate thephoto
in ways thephotoManager
may not be prepared for and vise versa - The
photo
could potentially get passed to other dependencies of thephotoManager
andviewControllers
and create Retain Cycles. - The
photo
could be changed tonil
when owners are expecting a value
A struct
passes copies into each freeing us of all these wories.
When should you not use a value type?
The above PhotoManager
should be a class
because it might need to be reference by several other instances at once, and the value then would need to be consistent.
Imagine having the PhotoManager
manage all the photos a User
object saves to his collection. If you create several instances of PhotoManager
then you risk Photo
objects being saved in one area not being saved in another area.
How Modern Apps Save Asynchronous Code from Being an Overwhelming Mess
And no, the answer is not the Result enum pattern.
You’ve seen it before:
We gotta do a lot of weird things here:
- We have to add extra
if else
brackets to distinguish betweenresult
anderror
- We have no built in way to check the state from elsewhere when
loadImage
is in progress - If I want to chain asynchronous calls together, more embedded brackets!
- Even switching to the
Result
pattern still requires a nested switch statement
Your Async Code should separate concern, label events clearly, avoid nesting brackets, and capture state.
Swift will eventually solve these problems using the Async Await Pattern.
In the meantime, PromiseKit is a wonderful solution to these problems.
Imagine a UIViewController
that displays a UIImage
full screen after its loaded from the web, but you want to hide all the heavy lifting from the UIViewController
:
Let’s look at what’s happening
- The
Promise
is injected into thePhotoViewController
as a captured state. - The View Controller doesn’t know anything about who called the
Promise<UIImage?>
increasing the reusability. stopAnimating()
is guaranteed to be called without having to check for each potential end of flow.- Brackets are not nested several layers . If you wanted to add another call onto the
Promise
, you could simply add another chain ofthen
.
How to Guarantee Safety and Reusability when passing Dependencies
Usually, your Classes
and Structs
need to know about other parts of your code.
This means your class requires dependency injection.
For example:
Above the photo
was injected into PhotoViewController
because it will rely on the photo
object to furfill its requirement: to display an image full screen to the user.
Let’s take a closer look at those types:
PhotoViewController
's responsibility is simple. It needs a UIImage
to display and a formattedDate
to show when the image was posted.
Exposing anything else creates unecessary risk.
So why inject a full FlickrPhoto
?
Especially if Joey, the friendly junior developer down the hall gets his hands on your code and thinks he’s helped by adding a captionLabel
to your PhotoViewController.
Cranky Pete then yells at him in code review. Joey cries in his cubicle and stress eats a giant bag of Flaming Hot Cheetos.
Poor Joey. It wasn’t his fault you didn’t inject only what the PhotoViewController
actually needed:
Injecting only the URL
and the Date
saves Joey from himself.
And as long as another Photo type adops the Photo
protocol, you’ll be ready to handle photos from anywhere like Flickr, Google, or heck, grandma’s photo library.
Taking it one step further with ViewModels
Your PhotoViewController
gets passed a Date
, but it doesn’t actually care about the Date
object itself. It only cares about the String
the Date
is formatted into.
A ViewModel
is best leveraged when converted basic data into something presentable, then converting the data the UIViewController
returns back into something the rest of the app can use.
We can format the Date
using an Extension
:
We can even go a step further leveraging Promises
or other state capturing objects.
Instead of a URL
, the PhotoViewController
only needs a Promise<UIImage?>
:
Now we have EXACTLY what the PhotoViewController
needs. It can respond to the current state of the Promise
to know whether it can show the image or show loading.
The PhotoViewController
also no longer is responsible for formatting the date itself.
Reducing what you know about a type’s dependencies applies to every layer of your app.
When creating a new type, document each requirement.
If there’s no current type that matches the exact requirement your components needs to function, create a protocol
that DOES meet the requirements and inject the protocol
.
How to Increase the Quality of View Controllers by Decoupling States and Events
Apps are getting BIG. Like six figure lines of code big. In rare cases, even seven figures.
Because they are getting big, modularization is becoming MORE & MORE important.
We’ll use whats called the Coordinator Pattern which Paul Hudson champions, and the company I work for, InMotion Software, implements in all its apps.
Using the modified Coordinator
Pattern, , we move all states and transitions outside a View Controller.
Take a look at a typical login screen:
We’ll use:
- Email
UITextField
- Password
UITextField
- Login
UIButton
- A
UILabel
to display an invalid email message. - A
UIActivityIndicatorView
for loading
We then have a few different states to keep track of. Things like:
- An invalid email was entered
- Both fields have been filled out and the LoginButton is ready
- Login was tapped and should attempt to authenticate.
And much more (as demonstated soon).
Often the UIViewController
will handle these states itself. The problem with this is when you have to reuse, refactor, or add new parts to the code.
When you remove state and event handling from a UIViewController, you improve reusability.
If we wanted to use a similar UIViewController
to your LoginViewController
but give it different behavior, that would now be easy.
When you separate states and transitions outside
UIViewControllers
, you make your states & transitions more unit testable.
You’d be able to write a clear unit test that verifies that returning an invalid email from the default state will ALWAYS result in an invalidEmail
state. No need to interact with Views
or UIViewControllers.
Think of your
UIViewController
as literalView
controllers. There only responsibility being to manageView
s and anything else should be done outside.
How to move state and state transitions outside your View Controllers
You’re going to create a Class
object in what is typically known as a Coordinator.
Here’s all the states your LoginCoordinator
has:
begin
when it first shows, what needs to happen?loginDisabled
when the login button is disabled until a valid email and password are addedinvalidEmail
when the user enters an invalid emailloginEnabled
a valid email and password are addedloggingIn
A user has tapped the login button and begun logging inloginSuccess
to show to the user the login succeededloginFailed
to show an invalid credential or other error messageadvance
when we’re transitioning to the next screenback
when we want to return to the previous screen (if any)
States, of course, can’t exist on their own — they need a way a move to other states.
We call moving between one state to another a “transition”. What other states can the user access from the current state?
It doesn’t make much sense to allow the user to move from loginDisabled
to loggingIn
does it? You can’t just begin logging in when you haven’t entered proper credentials.
Here’s the complete look at state transitions:
begin
will only ever transition tologinDisabled
.loginDisabled
can transition toinvalidEmail,
loginEnabled
andback
invalidEmail
can transition tologinDisable
loginEnabled,
andback
loginEnabled
can transition toinvalidEmail
,loginDisabled
,loggingIn
, andback
loggingIn
can transition tologinSuccess
andloginFailed
loginSuccess
can only transition toadvance
loginFailed
can only transition tologinEnabled
(the back button being disabled until the message is dismissed)advance
cannot transition to anything since we leave the scope of the controller
There’s plenty of ways to represent each state. Apple recommends capturing state using an enum and I agree:
Then we build our LoginCoordinator
that manages a LoginViewController
.
This way, if you change the UIViewController
you use for logging in, you can still plug it right into this without making other changes.
If you want to take it a step further (which I have in professional settings), you can make transition(to:)
throw an Error
object if it does not successfully transition to the given state.
This adds an extra level of defensive programming that verifies the correct state was reached.
How View Controllers managing Segues is Harmful and What to do about it
Let’s revisit our LoginViewController.
Imagine a user has logged in and is segueing to the next screen.
Most users are heading to a HomeViewController
to see a normal shopping experience.
But Todd is addicted to crab cakes and buys them EVERY TIME he shops. Todd is about to see a juicy crab cakes “buy 2 get 1 free” promotion in aPromotionViewController
.
You could handle that situation like this:
The problem with this solution: We’ve baked in which screen should show directly in the LoginViewController
. You’ve now coupled the Login with the Promotion and Home Screen.
Imagine Todd just moved to Florida from Colorado where crabs are more abundant and he can feed his addiction and now wants to confirm his new address.
Now you gotta add a segueToLocationChange()
. Which means we’ll also need to inject more dependencies into LoginViewController
to detect the user’s location.
We can imagine that if else
chain becoming unsustainably long with each new change.
Our code gets uglier and less reusable the more we add.
If we plug in all your segues into the LoginViewController
, you won’t have any chance to reuse it without rewriting a lot of code.
How to properly handle transitions to new View Controllers
There are three main ways we handle transitions between each UIViewController
.
1. If closely related to the original View Controller, you can treat a newly shown View Controller as a new state of the original View Controller.
Let’s revisit our LoginCoordinator
and add Forgot Password functionality.
We’ll add a ForgotPasswordButton
that launches a ForgotPasswordViewController
which asks the user to enter his email and confirm.
Upon confirmation, he’ll be notified an email is on its way. The ForgotPasswordViewController
will then dismiss.
This adds only few new states:
forgotPassword
forgotPasswordInvalidEmail
forgotPasswordLoading
forgotPasswordSuccess
forgotPasswordFailure
Seeing as there’s only 5 new states, we can add them safely to the LoginCoordintor
without getting overwhelmed.
Forgot Password is also naturally coupled to Login. Its usually the only flow it makes sense in.
In this case, the LoginCoordinator
will handle the segue
from the LoginViewController
to the ForgotPasswordViewController
itself.
2. If a segue between two View Controllers managed by their own Coordinators is simple, the Coordinators should transition directly from one to the other.
When each View Controller is complicated enough to be managed by its own Coordinator
, sometimes a Parent Coordinator
will own children Coordinators.
In the case where our LoginViewController
is done logging in and handing off to a HomeViewController
which is managed by a HomeCoordinator,
then it should be a simple handoff:
Often times you can manage coordination from an AppCoordinator
managed by the AppDelegate
.
3. If a Segue is custom and complex, a transition will require an Interface Controller.
Interface Controllers are another solution I’ve been informally using for a long time, but Matteo talks about is his Lotus MVC pattern.
A good example is when viewing a photo album and tapping a Photo. The Photo will do a screen takeover and become giant.
In our example, let’s pretend we wanted to use a custom transition to jump from the HomeViewController
where the user is browsing thumbnails of items he might buy at a grocery to view the item in a full screen image using PhotoViewController
.
The transition wanted to show the thumbnail growing large and becoming full screen with the background fading to black.
Here’s how that might look with an interface controller:
Then, in an AppCoordinator
that manages the transitions…
Learn Other Things that Help you Build a Great Career using App Development
Learn to Earn More, Use the modern coding techniques, App Entrepreneurship and more by signing up here.
📝 Read this story later in Journal.
👩💻 Wake up every Sunday morning to the week’s most noteworthy stories in Tech waiting in your inbox. Read the Noteworthy in Tech newsletter.