SwiftUI — Apple’s declarative UI framework that works across all its software platforms —  is young and full of problems, but it's also magically simple and crazy fast to build once you get the hang of it.

So we really shouldn't be asking whether it is or isn't flatly "production-ready." Rather, we must evaluate whether it's a strategic choice depending on our specific circumstances and projects.

Here's some variables you should weigh to decide whether to use SwiftUI in your production apps.

Do you have time to learn?

If you've never used SwiftUI before (or some other declarative UI framework), don't expect to hit the ground running. While you may have heard it's simpler and easier to use than traditional frameworks like UIKit, there's about a million caveats.

First of all, the declarative style that makes it so simple also has a steep learning curve. If you're used to UIKit (or programming in pretty much any other context), your habits are going to resist embracing the paradigm in ways you might not even expect.

Basic example

Let's first consider a basic example of a button in SwiftUI:

Button("Button Title") {
        /// Insert button action here
}

Seems pretty straightforward, right? You pass the title of the button in the initializer, and a closure for the button action. Great.

But now lets say you want to present a view modally on tap of the button, like so:

In UIKit, your button would have a callback that looks something like this:

func didTap(_ button: UIButton) {
        let viewController = DetailViewController()
    present(viewController, animated: true, completion: nil)
}

In other words, the button is performing the act of "presenting" the view controller. But in SwiftUI, everything is declared as a function of view state. There is no concept of "doing something" except as a modification of the universe of view state that has already been declared.

So first, we'd need to add the button to our view:

struct ParentView: View {
        
    var body: some View {
        Button("Present View") {
                        /// Insert button action here
        }
    }
}

Then we'd need a variable that tracks the state of the view being presented, and we'd have to modify that variable in the button's closure.

struct ParentView: View {
    
    @State private var isPresenting: Bool = false
    
    var body: some View {
        Button("Present View") {
            self.isPresenting = true
        }
    }
}

But where is the view actually being presented? We need to declare it in the body as well and attach it to our variable. SwiftUI uses the "sheet" modifier for modally presenting views:

struct ParentView: View {
    
    @State private var isPresenting: Bool = false
    
    var body: some View {
        Button("Present View") {
            self.isPresenting = true
        }
        .sheet(isPresented: $isPresenting) {
            ChildView()
        }
    }
}

And the sheet modifier can actually be attached to multiple places in your view declaration — it has nothing to do with the button itself. Though not all of the following are preferable, they each function the same (at the time of writing this article):

struct ParentView: View {
    
    @State private var isPresenting: Bool = false
    
    var body: some View {
        VStack {
            Spacer()
            Image(systemName: "sunrise")
            Button("Present View") {
                self.isPresenting = true
            }
            Text("Tap to present a view modally")
            Spacer()
        }
        .sheet(isPresented: $isPresenting) {
            ChildView()
        }
    }
}
struct ParentView: View {
    
    @State private var isPresenting: Bool = false
    
    var body: some View {
        VStack {
            Spacer()
            Image(systemName: "sunrise")
                .sheet(isPresented: $isPresenting) {
                    ChildView()
                }
            Button("Present View") {
                self.isPresenting = true
            }
            Text("Tap to present a view modally")
            Spacer()
        }
    }
}

Broader implications

The point is, if you're used to UIKit and other kinds of imperative programming (as most of us are), presenting a view in this way feels pretty weird and unnatural.

And while this particular example might not seem like a huge deal, thefeeling will compound as your app becomes more complicated and you find yourself attempting to implement less basic functionality.

I spent six months with SwiftUI on side projects before I considered using it in a production app. And I ran into all sorts of weird issues related to its declarative nature and beyond. It was only once I developed an intuition around it that I was able to take advantage of the gains in productivity it can offer.

If you're not there yet, you should avoid attempting to use SwiftUI on features or projects with significant time constraints. It can take many added hours to wrap your head around the paradigm shift necessary to implement non-trivial features. But once you do, it can allow you to blaze (for certain things) like never before.

Are you ok with UI limitations?

The other glaring problem with SwiftUI is that it's quite incomplete (and sometimes quirky), especially the version that supports iOS 13. So if you choose to use it in certain contexts, you're going to have to employ a number of workarounds to get some really basic UIKit functionality.

Imagine you need to display a list of currencies in your app, for example. That's simple enough.

But what if you want the separator lines between rows to be inset so they start at the beginning of the text rather than the icon, as is the case in Settings and many other apps?

You can't. You also can't add a background image or color to the whole list, or other basics like custom swipe actions.

Well, strictly speaking you can, but you can't do so in a straightforward way. You'll need to employ workarounds using APIs like UIAppearance or a third-party tool like Introspect that iterates the view hierarchy to find the UIKit view in question. And who knows what workarounds will break as iOS is updated.

On top of that, if you want a loading indicator (UIActivityIndicatorView) or share sheet (UIActivityViewController) or various other standard components, you'll need to port them yourself using UIViewControllerRepresentable or UIViewRepresentable. And that can get old after a while.

But with all that said, SwiftUI may still be a reasonable choice for your app. If you can bend on certain design requirements or are in a position you can take minor risks with some workarounds, the speed gains in development for certain kinds of apps might still outweigh all the quirks you'll run into. And if there's some view where you absolutely can't bend, you can still implement isolated views in UIKit and present them from SwiftUI. You're likely going to have to mix and match no matter what (e.g. for things like activity indicators), so it's not a stretch to have a handful of custom views that aren't purely SwiftUI anyway.

Do your views need UIKit performance?

In some cases, the views you build with SwiftUI will not be as performant as their UIKit counterparts. And this may or may not be ok depending on what you are trying to accomplish with your view.

For example, let's say you want to display a collection of items that scrolls horizontally like something you'd see in Netflix or the Apple Music app:

You might consider a UICollectionView, which automatically recycles views that have moved off screen. But in order to do that, you'd have to bring that to SwiftUI with UIViewRepresentable, which is doable but potentially time-consuming.

Alternatively, you can just use a scroll view with a horizontal stack as such:

struct HorizontalCollection: View {
    
    /// The items to be displayed in the collection
    let items: [Item]
    
    var body: some View {
        ScrollView(.horizontal) {
            HStack {
                ForEach(items) { item in
                    ItemView(item)
                }
            }
        }
    }
}

Unlike a collection view, all items will be loaded into memory at once. That means if you have 1k items, 1k views will live in memory upon load – which really sucks for performance. Even if you use a LazyHStack, introduced in iOS 14, views will not be loaded until they appear, but they'll remain in memory thereafter.

That said, if you only ever have a few, inexpensive items in your collection at a time, the simple SwiftUI approach may be totally justified. It may lead to no discernible impact on the UX or risk of iOS terminating your app due to memory overuse. On the other hand, if your collection is displaying a bunch of images or a large number of even seemingly inexpensive items, you may be obliged to rely on the out-of-the-box performance benefits of certain UIKit components.

The key is to be aware of the performance differences of the SwiftUI components you're considering using as early in your implementation as possible. If your app only has a few views that require something like a collection view, then the occasional UIViewRepresentable implementation may be the way to go. However, if most of your app requires lots of cell re-use and lazy loading with lots of nuanced customization, you may spend more time fighting SwiftUI than you would just using UIKit exclusively.

What's your risk tolerance for surprise breakages?

There's always the risk that your implementation of some feature might break from one version of iOS to another. But with SwiftUI, the degree of such bugs seems to be a bit more dramatic.

For example, I recently built one of my SwiftUI apps that was running perfectly on iOS 13 with Xcode 12, and a whole bunch of stuff broke, including two big items:

  • Nearly all dismiss buttons in my modally presented views stopped working
  • Modal views presented from the same screen were not correct. That is, the wrong view was being presented.

Turns out, SwiftUI re-implemented the way it presents and manages sheets. So the previous way of presenting and dismissing multiple modals from a single view no longer worked.

struct ParentView: View {
    
    @State private var isPresenting: Bool = false
    
    @State private var current: PresentedViewOption
    
    var body: some View {
        VStack {
            Button("Present View 1") {
                self.current = .option1
                self.isPresentingView = true
            }
            Button("Present View 2") {
                self.current = .option2
                self.isPresentingView = true
            }
            Button("Present View 3") {
                self.current = .option3
                self.isPresentingView = true
            }
        }
        .sheet(isPresented: $isPresentingView) {
            self.view(for: current)
        }
    }
    
    ...
    
}

With the above code, neither the "isPresenting" nor the "selection" state variables were being updated as expected, so I had to instead re-implement the code with a different sheet modifier and tweak the way I passed the state to the subviews.

.sheet(item: $selection) { item in
     self.view(for: item)
}

Because my app was relatively small, it only took me a few hours to go through and fix all the bugs for both iOS 13 and iOS 14. But if your app is large with a substantial number of views (and developers), the cost and complexity of these kinds of fixes might not be acceptable for your constraints (at least at this stage in the evolution of SwiftUI).

It's only iOS 13+

It almost goes without saying that SwiftUI supports only iOS 13 and above. So anything you integrate into your app that uses the framework will not run on users with older versions of iOS.

As of June 2020, Apple Reports that 92% of devices introduced in the last 4 years use iOS 13, while 81% of all devices (including the oldest) use iOS 13. Some businesses are not willing to part with that remaining 8%-19% of users.

While this is fundamentally a business decision, it’s our job as developers to inform the stakeholders and product managers of the time and cost implications of choosing one option or another. While 8%-19% of users is absolutely significant, you need to compare that with the costs and time implications of supporting those users. There are many cases in which the added expense outweighs the benefit. For example, if you’re an early adopters with a small team and limited budget, getting to MVP for fewer users can vastly outweigh the importance of attracting every possible user early on, especially if choice of technology makes development materially faster.

You don’t have to go all-in

Finally, while this was mentioned throughout the above considerations, it’s worth addressing directly.

You have control over how much SwiftUI you use in a given app, and you can always fall back to UIKit when needed. And your options fall into essentially two categories, each with their own risk profiles.

Adding bits of SwiftUI to existing apps

This is the lowest risk way to engage with SwiftUI in your production apps. UIHostingController allows you to layout individual views and view controllers in SwiftUI and present them back in UIKit just as you would any other view or view controller. This is a great way to take advantage of SwiftUI benefits without being subject to as many quirks as you would otherwise.

The only caveat here is time constraints if you’re totally new to SwiftUI. Some items will take a long time to implement before you’ve internalized the declarative structure, and may lead to seemingly inexplicable bugs or crashes that will take more time to figure out. Work through things like this on your own time on projects where deadlines are loose. Until you’ve reached a certain level of familiarity, introducing it into even one feature might lead to unexpected setbacks.

For example, let’s say you have a UITableView for which you’d like to use SwiftUI just for the cell layouts. This is technically doable, and SwiftUI makes the layout code way simpler than dealing with autolayout constraints. So you go ahead and implement it, and wrap your SwiftUI view in a UIHostingController to instantiate it from your UIKit code. The problem is re-use. You only have programmatic access to the SwiftUI initializer and the view auto-generated by the UIHostingController. So how do you change the underlying view without instantiating it every time a cell is dequeued? Not sure you even can in a non-hacky way, so you might end up scrapping your SwiftUI view and using auto-layout anyway.

You want to run into problems like this in your free time, not when you have a deadline hanging over your head!

Adding bits (and chunks) of UIKit into SwiftUI apps

Any app that is going to make it to production is likely going to need to reference UIKit at some point. As I mentioned before, native SwiftUI implementations of basics like a loading indicator and share sheet require referring to UIKit, and there may be times where performance-tuned components are needed for acceptable scrolling and memory usage. Unless your app is exceedingly simple, it’s unlikely to be purely SwiftUI no matter what.

And because of UIViewControllerRepresentable and UIViewRepresentable you get to reap the benefits of UIKit as much as you need. But again, you should wait until you’ve reached a certain level of experience with SwiftUI before making it the basis of your app, and ensure your design requirements aren’t so nuanced that it wouldn’t be worth your time to deal with the quirks of SwiftUI at all. At this point, smaller apps with smaller teams are more likely to be ok using SwiftUI, while more complex, larger apps with rich design requirements are probably safer resting on a foundation of UIKit.

Where to go from here?

I personally love using SwiftUI. The declarative style, easy composition of views, cross-platform reach, and various other features make the development of lots of types of views extremely simple, elegant, and fast. I like it so much that I’m willing to put up with what seems like a ton of quirks, even in some production apps, as the framework matures and the ongoing hiccups are reduced.

In a current production app pending release, nearly the entire app is written in SwiftUI. I’ve run into a number of the problems I mentioned above, but for me, as someone who has already undergone the mental paradigm shift, the productivity gains were worth the minor setbacks, which really weren’t so hard to fix. That said, this is for a relatively small app seeking early product-market fit. If you already have millions of users, elaborate features and a large team, adding SwiftUI in the mix might be more headache than it’s worth. You need to weigh the nuances for yourself and the risks your business can tolerate.

Discussion

Categories
Share Article

Continue Reading