Fri, 10 Feb 2023 14:09:00 +0000

Designing for reusability and testability

Portable Architecture

TLDR; It's important to represent your swift logic in a portable way that allows you to easily reuse and test it across as many platforms as possible.

In the previous post I discussed the state of running Swift code outside of Apple's platforms and the tradeoffs for doing so. One of the bigger take aways from that post was the reasoning behind why Swift is mostly used on Apple's platforms.

Simply put, Apple makes it easy to use Swift on their platforms because they provide all of the frameworks and libraries needed to make tangible apps that run out of the box on their ecosystem.

They make it easy to create user interfaces and hook up back ends and all sorts of stuff to your app without having to navigate the vast sea of developer resources elsewhere. They propose design patterns on how to organize your code and best practices too!

But good luck running that same code outside of your Mac unless you're not relying on their libraries...

I closed off the post mentioning how leveraging code that runs on foundation (the main set of standard libraries shipped with Swift everywhere) should guarantee that your Swift code runs on non Apple ecosystems. But that puts the burden of finding an alternate UI / Graphics framework on us.

Before we get to the point where we're drawing UI's and all sorts of graphics in Swift on non Apple platforms, I figured I'd discuss a general strategy to use when writing our code that will eventually be doing this.

This post will discuss proposed (yet not required) Architectures to use when writing portable Swift code. We'll go through it's pro's and cons as well as look at their overall structure.

Hopefully this will set us up for success throughout the rest of our journey by putting us in a space to think about what it means to write portable code and why!

Architecture??

I'd like to preface this architectural discussion by briefly mentioning what I mean by architecture.

Make no mistake, when it comes to writing Swift code (or any code for that matter), theres nothing stopping you from just chucking everything you want into a single file until it works.

let x = 1
let y = 1
let z = x + y
print("Hello World! The number inside of z is: \(z)")

This works perfectly fine in a random swift file with nothing else needed to make magic and show this on some console

In fact, Apple offers Swift Playgrounds as a means to write code snippets here and there and quickly test simple things, in a single file (sometimes more). This will only ever take you so far though.

After you gain enough experience writing code in the real world, you eventually start to realize that most of the difficulty of writing code, is dealing with old code. Whether it's moving into a pre existing code base written by someone else, or resurfacing an old project of yours.

I forgot where I read someone talking about the feeling of reading old code they wrote years ago. They described it like reading a murder mystery thriller where the culprit turns out to be yourself in the end.

That's part of the reason why the industry favors the use of Architectural patterns as a means to try to wrangle and standardize big projects. The hope is new and old contributors to a code base, will be able to figure their way around it because of the familiar aspects to it.

And that's half the battle.

Apps the Apple Way!

In the beginning there was MVC. For a moment, try to transport yourself back to 2001.

The world was a very different place, the internet was still in its infancy, the ipod saved humanity and the modern day software practices that we take for granted today, were still being designed.

Apple started trying to standardize the way developers wrote apps on their platform, long before this point, thanks to the foundation set in place by NextStep. For the longest time Apple pushed MVC as the design pattern of choice for their apps. It made the most sense for them (or the people hired to make sense of it all).

The idea being, 'Hey, we provide a standard way for your apps to launch, all you gotta do is write the windows and content from that point on'. Thus we get this structure showing how to outline the screens of your app.

The quick summary is:

You have your Model which has data you're going to feed to your screen.

This can be something trivial like maybe a model representing a dog, that has information about it like:

How old it is

It's name

Color of fur

You get the idea.

Your View (or rather, VIEWS) are the things drawn on the screen that you interact with. This can be buttons, images, text boxes, switches, URLS, YOU NAME IT!

And last but not least, you have your Controller, or the thing that's stiching everything together. Your controller (or how they became commonly known as ViewControllers) should be able to pass information from the Models to your Views and just make it all work.

Conceptually this works. Practically, it does too for most cases. But humans do a great job of messing things up which is why we exist in a never ending game of cat and mouse where we constantly build and refine what we previously built. It's a beautiful type of chaos.

Optimism with experience aside, the industry moved towards other patterns because of some of the limitations of this design. The biggest one being that it was easy to turn from MVC to MVC (as in, MASSIVE View Controller).

People just started chucking everything in their view controllers to the point where they began to become almost unrecognizable former shells of themselves. Horrible attrocities being committed every second by developers under tight deadlines or just suffering from a lack of enthusiasm and care in their practice, became the norm.

View controllers would balloon in size, to thousands of lines (in the worst case). You risked breaking this giant jenga tower anytime you wanted to go back in and make changes. It's pretty morale draining to be honest. You're not creating so much as you're fighting your creation, which goes against the point of why we establish design patterns in the first place.

But worst of all, the way this pattern was introduced was inherintly not portable.

Whenever you try to create a new ViewController in XCode with Swift (or even just a new app with a pre populated ViewController), you traditionally start with something like this:

import UIKit

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        print("Hi! I loaded and you can probably see me on the screen soon!")
    }
}

The only thing you could reuse from that snippet of boilerplate Apple Swift code, is the harmless print statement. Everything else is bound to Apple's platform. As a rule of thumb, if you see anything with UIKit, or prefixed with UI, chances are, it's apple specific.

These are an iOS / Mac Developer's first baby steps. From step 1 they're forced to stick with Apple's ecosystem with each and every line they write. By the time they're running, this has only gotten more convoluded.

What else is out there?

As the industry evolved and people continued to make apps on other platforms such as Android and any number of web / desktop technologies, so to did our patterns.

No matter where you were working in, the need for standardization and making your life easier when jumping into new and old projects, only became greater!

Other patterns emerged with improvements to this traditional design. Most noteably:

MVVM (Model, View, View Model)

and VIPER (View, Interactor, Presenter, Entity and Router).

I won't go deep into these considering that you could make a lengthy blog post about each of them.

Instead I'll trust you to look them up if you have any more questions. For now, I'll summarize what they do better than MVC.

Both of these design patterns were aimed at solving many of the shortcomings of MVC. With MVC, it's easier (but not impossible to avoid) to couple the state of things on your screen and views, against all the other moving parts like your data. This can make it hard to test because you're essentially morphing everything into a black box.

Although how to write tests is out of scope for this article and deserves one of its own, it is worth mentioning that this black box mentioned above is harder to test.

MVVM and VIPER propose different strategies to avoid making this black box, and instead making several smaller boxes that could be reasoned with individually, throughout your app's development.

MVVM

MVVM proposes that you represent your models how you would in MVC (like that dog model above). As well as your views. But to try to keep all the state of this, inside another box (the view model) that can then be leveraged in screens or wherever you want in your app.

The core idea is to decouple the state into its own spot, and let it keep track of what ever it needs. Like how many Dogs (again, refering the model above) you're keeping track of, whether or not a button or a label should be fed certain info (without having to worry about even knowing that the button or label exists), etc.

Then you have a ViewController or a screen or what ever, listening to the state of the ViewModel and passing things off to Views and other like minded stuff, appropriately.

This makes it easier to test because if you decouple your state from the original ViewController in MVC, into this ViewModel object, you can write tests against the ViewModel object and not care about the Views or UI that has to be running.

You can proceed with the assumption that the 'state' of the ViewModel (even if its just a variable keeping track of a dog) is where it needs to be when you need it, and that your UI Code will know what to do with it.

Furthermore, by keeping your logic encapsulated like this, and not having it care about your Views (or UIKit apple stuff commonly found in ViewControllers), you essentially are building most of what you need without having to rely on Apple's UI Frameworks.

You can reuse this state and other model data, in a code base that has Apple specific libraries like UIKit, or maybe a home grown alternative!

VIPER

VIPER tries to do what MVVM does, but takes it a step further. It tries to abstract away from the navigation between screens or elements in your app that move. On iOS, it's common to represent screens as ViewControllers, and to present new ViewControllers every time you want to show the user a new screen (at least that's how its intended).

VIPER also tries to make boxes for the different interactions on your screen or across different' UI elements. It basically tries to encapsulate everything so you can inject it into any other system with more clear direction.

Although VIPER may appear overly verbose at times, it still does a decent job achieving what it set out to do. I personally don't start off with VIPER unless I see something balooning up in scope or size, and try to find a middle ground with MVVM or similar patterns.

Closing Thoughts

After discussing the pros and cons of common architectures used when designing on Apple's ecosystem, we got to analyze a little deeper into what types of thinking we're going to need to adopt when figuring out how to use Swift everywhere outside of Apple's walled off garden.

We saw some historical precedence into why things are the way they are today when writing Swift and also some other patterns that emerged outside of Apple's originally proposed MVC design system, to tackle it's shortcomings.

We also discussed how we can leverage patterns like MVVM and VIPER to write code that technically does the stuff we want our apps to do, without being too tightly coupled to the UI, which gives us the wiggle room we need to make code that will be ready to run on other platforms that can't leverage Apple's rich UI libraries and frameworks.

As you can probably guess, these architectures discussed will eventually help serve as the basis to how we can sanely write Swift outside of Apple's platforms and you'll be sure to see some of their concepts in action in future posts.

Till next time!