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

14 min readApr 3, 2019

--

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.

Me Basically

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 ViewControllers 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

Imagine that stone is really valuable and not something store bought

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.

Learn more about me here.

5 Important Swift Patterns that will Immediately Improve your Apps Scalability

You too could own a couch and a computer stool thingy if you write great swift architecure.

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.

Structsin 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:

The print statement will return false

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 the photo in ways the photoManager may not be prepared for and vise versa
  • The photo could potentially get passed to other dependencies of the photoManager and viewControllers and create Retain Cycles.
  • The photo could be changed to nil 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 between result and error
  • 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 the PhotoViewController 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 of then.

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.

A formatted date and Image is all thats needed

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:

The Photo protocol makes things a bit better

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:

Put this logic outside the ViewController where it can be reused easily

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?>:

You might make this a Class if you expect it to be mutable

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:

Imagine seeing ‘Invalid Email’ before typing anything…

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).

Transition from the default state to a login loading state

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 literal View controllers. There only responsibility being to manage Views 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 added
  • invalidEmail when the user enters an invalid email
  • loginEnabled a valid email and password are added
  • loggingIn A user has tapped the login button and begun logging in
  • loginSuccess to show to the user the login succeeded
  • loginFailed to show an invalid credential or other error message
  • advance when we’re transitioning to the next screen
  • back 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.

It doesn’t make sense to move from loginDisabled to loggingIn.

Here’s the complete look at state transitions:

  • begin will only ever transition to loginDisabled.
  • loginDisabled can transition to invalidEmail, loginEnabled and back
  • invalidEmail can transition to loginDisable loginEnabled,and back
  • loginEnabled can transition to invalidEmail, loginDisabled, loggingIn, and back
  • loggingIn can transition to loginSuccess and loginFailed
  • loginSuccess can only transition to advance
  • loginFailed can only transition to loginEnabled (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:

Associated Types could be used for loginSuccess(User) or loginFailed(error)

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:

Pass off from

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.

--

--

1M Downloads achieved, iOS vet. Learn to master the app development business: www.robcaraway.com.