The Ten Commandments of iOS Development
It is essential to be familiar with best practices and follow a set of core principles for iOS development if you seek to become an iOS architect. To make it easier to learn the best practices of iOS development, I have boiled them down to what is now known as “The Ten Commandments of iOS Development”. The name is a semi-joke, as these commandments serve as a Polaris star. That is, key principles to guide you to do iOS development according to best practices rather than strict rules that should be mindlessly followed 100% of the time.
This idea was inspired by iOS-factor and I decided to keep it short (and catchy!), simple and focused on the essential principles.
To ensure consistent and high-quality iOS development, I recommend following these ten principles:
- Thou Shalt Modularize Thy Apps
- Thou Shalt Keep Thy Apps Performant
- Thou Shalt Favor Composition Over Inheritance
- Thou Shalt Favor Local Over Remote
- Honor Thy Remote Configuration Capabilities
- Thou Shalt Automate Thy Deployments
- Thou Shalt Not Commit Without Automatic Linting/Formatting
- Remember the Backwards Compatibility, to Keep It Holy
- Thou Shalt Not Take the Name of Crash Reports in Vain
- Thou Shalt Have No Other Gods Before Human Interface Guidelines
I have found these guidelines being appropriate in the projects I've worked on, but some of the points may vary in your specific situation.
“I'll grab some coffee while the app is compiling.” - A phrase like this is a red flag. Your architecture may be highly monolithic. Being able to create, run and test parts in isolation is essential, especially in large scale projects, if you don't want one change in the codebase to trigger a recompilation of the entire app.
A common solution for maintaining speedy compile-time is by modularizing your apps. Modularity is critical for achieving a scalable and maintainable app over time. You can split your app into several modules in various ways. A common approach is splitting the app by feature, meaning different feature teams can recompile, test and run their feature modules independently. You can create modules in different ways. You can use frameworks in Xcode or dependency managers such as Cocoapods, Carthage or SPM, etc.
A modular architecture is going to add additional complexity, so be conscious if this decision fits the needs of your app. But as the codebase grows, it will result in decreased compile-time as well as enforced and clear separation of areas/responsibilities.
Even the most compelling app will have a hard time retaining users if it does not perform. To ensure we optimize for good performance when we develop apps, here are some principles to follow.
- Measure, Measure, and Measure Again. Use those instruments (Allocations, Timeprofiler, Leaks, and Activity Monitor) to analyze your performance issues, memory leaks, functions blocking the main thread, etc. for improved measured performance.
- Delay any initialization that's not immediately needed to speed up your launch time. Making your app responsive to commands quickly provides a better experience for the user.
- Understand and use the optimization tips suggested in Writing High-Performance Swift Code by Apple.
- Embrace the perceived performance. Use all the tricks in the book to make it seem like your app is running faster and better. Do things like:
- Showing a launch screen that looks like the initial screen.
- Showing shimmer effects (instead of showing a spinner) when waiting for an HTTP request to complete. Take a look at Facebook's Shimmer.
- Avoid micro-optimizations until later. They’re often not the real culprit. Focus on the bigger picture first.
Classes may achieve polymorphic behavior and code reuse by containing other classes that implement the desired functionality instead using inheritance.
In languages such as Swift, it's not possible to inherit from multiple classes. That limits our ability to compose our apps of tiny single-purpose components using inheritance/subclassing. Instead, we have protocols to do composition, because your classes can inherit from multiple protocols (even protocols can inherit from multiple protocols). We can then add functionality to our protocols using protocol extensions, as this allows us to give our protocols implementations and thereby achieve reusable components.
- WWDC Session: Protocol-Oriented Programming in Swift
- WWDC Session: Protocol and Value Oriented Programming in UIKit Apps
There are a variety of reasons to delegate more business logic to be executed locally on-device, instead of fetching the calculated results from a remote backend. Here's just a few for why you should optimize for running tasks locally:
- It reduces the data usage, as some users have a monthly data limit
- The connection may not always be reliable which can hurt the app experience
- Privacy may be a concern depending on which kind of data is being sent
Also, you will benefit from making your local experience better. Your app shouldn't require an internet connection for everything - sometimes there will be no connection at all. For example, a social media app should be able to show historic data in read-mode, instead of becoming completely unusable without an internet connection. Lastly, limiting the number of requests to the backend will simply enhance the user's experience by improving responsiveness and reducing load time.
Keep configuration out of the codebase as much as possible, that is, API-keys, URLs, feature toggles, etc. There are several reasons that we want to be in control of them remotely. API-keys can expire and URLs can change. Remote feature toggles (defined by Pete Hudgson as operational toggles – read this article from Martin Fowler's blog) is a powerful concept. Here are a few reasons for having remotely configurable feature toggles:
- Remotely disable a feature in production
- A/B-testing to enable certain features only for a subset of your active users
- Canary-releases to reduce risk by rolling out a change to a small subset of users before making it available to everybody.
- Clean iOS Architecture for Feature Toggling
- Firebase Remote Config as a configuration management service.
Don't let your deployments to TestFlight/App Store be a process that requires one guy in the office to be involved. You should aim to automate the process to be able to do a release from any machine, simply by triggering your CI system and let it work for you.
Get yourself familiar with Fastlane to start with. It's a set of tools for testing, building, code signing, deployment to TestFlight/App Store and much more. Check out this article on how to set up a complete continuous deployment pipeline with Travis CI and Fastlane.
If you're in a large organization, there are many reasons to virtualize and orchestrate you build machines using Docker and Kubernetes. The main advantage is the ability to easily and quickly scale up your build infrastructure when the load increases. Luckily, we got a cloud solution called Orka by MacStadium last year that allows you to orchestrate macOS in a cloud environment using Kubernetes and Docker and achieve exactly that.
Automate code style and formatting using SwiftLint and/or SwiftFormat and hook it into the development flow using Git hooks and Danger. Don’t waste your time by discussing the styling rules over and over in the pull requests. Just automate it and focus on the essentials - providing the best user experience.
- Automate enforcement of code style and formatting using SwiftLint and/or SwiftFormat
- Use Git hooks to automatically integrate the formatting and linting as part of the workflow
- Use Danger to check the linting of changed files and give warnings in the pull request (optional)
- Discuss the code style and formatting rules in the pull requests – A lot of nitpicks in pull requests might be a smell that something is not standardized or (even better) automated
Yes, I know, SwiftUI, Combine, Property-Wrappers (and all other things we got last WWDC) are new and exciting. But hold your horses, we don't want to mess with our backwards compatibility just yet. There's still a large number of devices that can't run iOS 13. Of course, depending on your user base, this may not matter. But in larger organizations, it can take a couple of years before they can start dropping support for iOS versions below iOS 13. Not to mention the risk of jeopardizing the stability of the app by introducing completely new, not yet battle-tested tools.
You could adopt SwiftUI in parts of your app with the
@available(iOS 13.0, *) attribute to annotate availability information while maintaining backwards compatibility. With this information, the compiler can ensure that APIs annotated like this are available to all platforms supported by the current target.
There's nothing worse than apps crashing in the hands of an end-user, and, unfortunately, there will often be unforeseen issues - even when all tests have passed. You should actively monitor if your users experience any crashes. Therefore, using the crash reports in Xcode (
Xcode ➞ Organizer ➞ Crashes) is a good place to start.
In larger projects, this might not be sufficient. Some tools allow you to combine analytics with crash reporting to minimize troubleshooting by giving you insights into the reproduction path. I suggest that you check out Firebase Crashlytics for this purpose.
Lastly, a powerful method is to collect diagnostics reports from users in-app. Check out Diagnostics by WeTransfer for an easy way to set this up.
Follow the Human Interface Guidelines from Apple as they serve as best practices when designing iOS apps.
Apple offers Human Interface Guidelines as a set of recommendations to designers and developers for producing good-looking, usable and consistent user interfaces. By following these guidelines, we are walking on the shoulders of giants and are more likely to improve the experience for users by making our apps more consistent and hence more intuitive and learnable. We also end up designing apps that integrate seamlessly with the rest of the iOS platform.
Sometimes, you run into edge cases that require you to bend the rules. In these cases, I normally advise developers to discuss it with one of the senior developers and then find a pragmatic step forward.
The commandments here are what I have experienced to be helpful for the projects I have been involved in. Your situation could be different, so you may want to adjust some of the commandments for your development guidelines. Nevertheless, having these guidelines defined in a simple and straightforward format will do a lot for more consistent and efficient iOS development.
Personally, the results for implementing these guidelines have been faster iOS development with fewer comments in pull requests. Overall, better code quality as these guidelines cover the most common scenarios (80/20 principle).
If you are interested in learning more about best practices, in a guided and interactive way, I recommend you apply for iOS Architect Accelerator to advance your career as a highly-skilled and in-demand iOS architect.