Articles, podcasts and news about Swift development, by John Sundell.

Deciding whether to adopt new Swift technologies

Published on 03 Nov 2019

As Swift developers, the ways we build apps and the tools that we use are constantly changing. Every year, there’s a huge amount of new technologies, tools, frameworks, and language features coming out — both from Apple and third party developers — and while many of them are both useful and exciting, it can sometimes be challenging to keep up with this very rapid flow of changes.

Since everything is moving so fast, and there’s no indication of things ever really slowing down — making decisions as to what new technologies to adopt becomes incredibly important, especially as a project grows in both size and complexity. This week, let’s take a look at a few tips and ways of thinking when it comes to approaching new tools and technologies — using the decision of whether or not to be an early adopter of SwiftUI as an example.

To be, or not to be, production-ready

Whenever a new tool, library or framework comes out, there’s one main question that tends to get asked really frequently: is this new piece of technology “production-ready”?

While that’s a great question to ask, the answers are most often very context-specific. What the criteria are for something to be considered production-ready tend to vary quite a lot, both from person to person, and depending on what sort of environment that the technology will be deployed in. For one person, production-ready might simply mean that “it seems to work when used”, while others might have a much more extensive list of requirements to be fulfilled.

So rather than trying to obtain a singular answer as to whether the technology we’re looking to adopt is universally production-ready, it’s often much more productive to ask a series of questions that’ll let us build up an idea of its current state — questions like:

In many ways, choosing to become an early adopter of anything — whether it’s hardware or software — always comes with a certain degree of risk. However, the more aligned our use case is with what seems to be the most common one, the lower that risk usually becomes — and the only way to really find out whether or not that’s true, is to do a little bit of research before we start writing any code.

Non-critical first steps

In general, a great way to start adopting a new API or technology is to start by deploying it in a fairly non-critical part of a project. That both reduces the risk of an essential feature breaking, and lets us ease our way into using the new tool — to learn about its strengths, weaknesses and edge cases before fully deploying it across the project.

For example, let’s say that we’re currently deciding whether or not to adopt SwiftUI, and that we’ve determined that it seems to be ‌production-ready for our use case. However, since fully embracing SwiftUI requires us to drop support for older operating system versions — we’d first like to test it in a way that lets us still maintain complete backward compatibility.

Like we took a look at in “Shifting paradigms in Swift”, one way to do that is to add all of our SwiftUI code behind availability checks — which will let us keep running our app on older system versions, by only activating our SwiftUI-related code when running on devices that support it.

Let’s say that our app contains a feature that lets us show some form of promotion to our users, and that it’s not something we consider to be essential for our overall user experience — so we decide to use that feature as our SwiftUI testbed, by implementing it as a View marked with the @available attribute:

import SwiftUI

@available(iOS 13, *)
struct PromotionView: View {
    var body: some View { ... }
}

Doing the above will give us a compiler error if we ever try to use our new PromotionView within code paths that aren’t guaranteed to only be executed on iOS 13 and above — so to give the compiler that guarantee, we’ll use an #available check at the call site, like this:

func presentPromotion(in presentingViewController: UIViewController) {
    // Using this statement will let us assume that the rest
    // of this function will only be executed on iOS 13 and above:
    guard #available(iOS 13, *) else {
        return
    }
    
    // We also add a dynamic feature flag that'll let us control
    // whether our promotion feature will be enabled at runtime,
    // essentially acting as an additional safety mechanism in
    // case something goes wrong and we need to disable it:
    guard featureFlags.enablePromotion else {
        return
    }

    // We can now use iOS 13-only APIs without any problems:
    let view = PromotionView()
    let viewController = UIHostingController(rootView: view)
    presentingViewController.present(viewController, animated: true)
}

To learn more about feature flags, check out “Feature flags in Swift”.

Deploying a feature like the above feature in production, and monitoring the results over a period of time, perhaps gives us the very best indication as to whether or not a given piece of technology is something that’s ready for us to fully adopt. Doing so also lets us start working out how to integrate the new tool with the rest of our code base in a highly encapsulated way — which is great in case we decide to not adopt the new tool after all, as it should let us delete our experimental code quite quickly.

Where’s the escape hatch?

When adopting new technologies, chances are quite high that we’ll discover gaps in what their various APIs offer — especially when compared to older tools that the new ones are aiming to replace. No matter how great a new piece of technology is, building out a diverse set of APIs that cover a lot of ground takes time — which is definitely something that we need to take into account when picking what technologies to adopt.

One thing that creators of tools and frameworks can do to mitigate this problem, however, is to build in some form of “escape hatch” — that enables us to temporarily step outside the realm of the tool in order to complete a given task. Having such a mechanism available heavily reduces the chance that we’ll get stuck while adopting that technology, and also future-proofs it, as we’ll be able to extend it to better fit our use case.

Again using SwiftUI as an example — since it’s fully backward compatible with both UIKit and AppKit, that essentially acts as such an escape hatch. If we encounter something that’s missing in SwiftUI, or if we want to bring some of our existing code into it, we can do so by adopting UIViewRepresentable. Since we can also freely mix and match SwiftUI-based view controllers with UIKit-based ones, we can selectively adopt SwiftUI for the parts of our UI that it works great for — while still falling back to UIKit (or AppKit on the Mac) whenever needed:

class ProfileViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()

        // This view controller is implemented using UIKit:
        let header = HeaderViewController()
        ...
        add(header)

        // While this one is implemented using SwiftUI:
        let list = ListView()
        let listWrapper = UIHostingController(rootView: list)
        ...
        add(listWrapper)
    }
}

To see the implementation of the above add convenience API for managing child view controllers, check out this Basics article.

Whether or not a given tool supports some form of escape hatch-like mechanism, and how much flexibility that it gives us, is also something that we’ll be able to gradually discover by adopting it slowly — feature by feature.

Conclusion

It’s close to impossible to universally assert that any new tool, library or framework is fully ready for all kinds of use cases out of the gate — as how well any new piece of technology will work depends highly on the context that it’ll be used it. However, by learning from others’ experience, by taking our time to experiment and try out the tool for ourselves, and by making sure that any new technologies that we choose to adopt has some way for us to temporarily work around its limits — we can greatly increase our chances of success, even if we choose to become early adopters.

It’s also important to point out that not being an early adopter is also a perfectly valid choice — and in many circumstances it’s even the right choice. New technologies will most likely always have problems, and while they might be exciting and fun to use, there’s rarely any harm in being a bit more conservative with our tech choices — especially when working on a larger, existing code base. Like always, it’s all about finding the right balance between moving our tech stack forward, without running into too many problems along the way.

To learn more about SwiftUI in particular, and how Apple is both using it internally, as well as some of their plans for improving it — make sure to listen to my podcast interview with Josh Shaffer, engineering director with the UIKit and SwiftUI team.

What do you think? How do you usually decide whether a given technology is production-ready, and what do you think about the suggestions in this article? Let me know — along with any questions, comments and feedback that you might have — either via email or on Twitter.

Thanks for reading! 🚀