SwiftUI in production

Introduction

It’s been a month (at the time of writing) since my app Soccer Puzzles has been live on the App Store, and a day since its basketball sibling Basketball Puzzles has made it there too. They are both developed in SwiftUI. A month of being live on the AppStore can give some input how Apple’s new UI framework is performing on the App Store.

TL;DR

Yes, SwiftUI can be used in production apps and it works pretty well, most of the time. However, there are some glitches. For more details, please read on.

The app

Although it’s a trivia / quiz game, the app itself is not trivial. It has around 10K lines of code, with a sync mechanism, in-app purchasing, game logic (unlocking levels, achievements), with a lot of data passing through it. It also has a drawing engine, which can draw different football (and basketball) formations, based on their positions.

Football (Soccer) 101

For those who don’t watch football much, the formation is similar to the architecture of an app – how the team is structured and organized together. There’s a separation of concerns, for example, the goalkeeper and the defence’s main goal is to stop the opposition from scoring goals. They are behind (data model), they usually give the identity and strength to the team and the captain is frequently coming from that part of the pitch. The strikers (views) are in charge of scoring goals, everyone sees them and usually they’re the heroes, but can’t do much without the support of the team. The midfielders (controllers) connect everything together and bring the dynamics to the game. There are many ways to organize these parts, like the classic 4-4-2 (MVC?), the most popular 4-3-3 (MVVM?) and the more complicated ones with many layers like 4-1-2-1-2 (VIPER?). Here are some visual examples, but there can be many different combinations.

The pros

SwiftUI is perfect for this task. If you split the area in different parts (and sub-parts) for goalkeeper, defence, midfield and attack and just use spacers and offsets you can achieve very nice representation of a football formation, without doing much calculations. If I had to do this in UIKit, for sure it was going to be a lot of work.

Bonus, while you go through the questions, there are some nice animations, for which I just had to call the animate modifier. Again, doing this in UIKit would’ve been a lot more work. The questions themselves have many different representations, depending on the question type. Using SwiftUI’s composition approach, it was very easy to reuse components, move them around and so on.

Another point is the design and UX, which was prototyped and created along the way. At one point, one view was at the top, and then it was moved at the bottom. In SwiftUI, that’s a very fast change, while in UIKit you would have to mess with constraints the whole time.

Working with Combine was also very nice experience. It helped a lot in the sync mechanism, where I’ve created custom publishers for my repositories. The repositories would first fetch and return some cached or bundled data and then sync with Firebase to get the latest changes. For a trivia app, where there can be mistakes in the data, this was very helpful.

Here’s a glimpse of how I did the repo publishers.

extension Publishers {
    
    static func repositoryPublisher<R: Repository>(_ repository: R) -> Publishers.RepoPublisher<R> {
        return Publishers.RepoPublisher(repository: repository)
    }
    
    struct RepoPublisher<R: Repository>: Publisher {
        
        private let repository: R
        
        typealias Output = [R.Entity]
        typealias Failure = Never
        
        init(repository: R) {
            self.repository = repository
        }
        
        func receive<S: Subscriber>(subscriber: S) where Failure == S.Failure, Output == S.Input {
            let subscription: RepoSubscription = RepoSubscription(source: self.repository,
                                                                  subscriber: subscriber)
            
            subscriber.receive(subscription: subscription)
        }
        
    }
    
}

final class RepoSubscription<S: Subscriber, R: Repository>: Subscription where S.Input == [R.Entity] {
    
    private var source: R?
    private var subscriber: S?
    private var isInitial = true
    private var cancelables = Set<AnyCancellable>()
    
    // skipped initializer for brevity

    func request(_ demand: Subscribers.Demand) {
        guard let source = source else { return }
        
        if isInitial {
            _ = self.subscriber?.receive(source.savedEntities())
            isInitial = false
        }
        
        _ = source.syncEntities().sink(receiveCompletion: { (value) in
            switch value {
            case .finished:
                self.subscriber?.receive(completion: .finished)
            }
        }, receiveValue: { (levels) in
            _ = self.subscriber?.receive(levels)
        })
        .store(in: &cancelables)
    }

    // skipped cancel for brevity 
}

MVVM and DI

I’ve used standard MVVM pattern in this app and Combine fits in very nicely there. The view models were @ObservableObject, which were created from the outside and passed to the view. Therefore, I won’t do any changes for iOS 14 and there’s no need to use the new @StateObject.

Nice way to do this is to use a hierarchy of dependency containers. Having containers that create the views and view models is also very useful while using the SwiftUI previews feature, because views and view models can have complex initalization methods. For example, if I want to create a FootballTeamView, that has FootballTeamViewModel, that has PlayersRepository, AnswersRepository, UsersService, that also have other dependencies, then the previews can get very messy.

What I did, was first using protocols for all the dependencies in the types mentioned above. Then, I’ve created a PreviewsContainer, that implements PreviewProvider. In this provider, I’ve created mock repositories and data, that’s displayed in the previews. Therefore, creating dependencies in the SwiftUI previews was very easy. The good thing is that since the PreviewsContainer implements PreviewProvider, it will not go in the release version of your app. Another benefit is that you can use those same mock implementations in your unit tests.

These are some of the benefits from working with SwiftUI / Combine, apart from the fun factor, which for a pet project done after-work hours is what drives you.

The cons

Of course, SwiftUI is not perfect. The thing I don’t like the most is the minor version differences and bugs that can magically appear, especially in the 13.0 / 13.1 versions. Therefore, you should test on all minor versions and even on multiple devices.

One concrete example, which I’ve found out after going to production and looking at crash logs – there was a crash on few iOS 13.1.2 devices. The crash logs were not very helpful, some AttributeGraph crash with no further details. However, if you pick that same iOS/iPhone combination, you can easily reproduce the crash in Xcode. Even then, it’s really hard to debug and figure out what’s the problem.

The brute force approach was comment out things until it stops crashing. It turned out that after I’ve added an id modifier to the list, the crash disappeared.

Another issue was the present modifier, which also had problems on iOS 13.1 versions. I have a loading view, which appears when in-app purchase or ad loading is in progress. However, on those devices I’ve only seen white screen, which was not very helpful both for the users (not being able to earn more coins to play the game), and for me, since I wasn’t able to monetize the app for those users. Again, it was very hard to debug this and the quick solution I came up was very silly – not use the present modifier for those versions (if someone knows something better, please share). You can use a conditional modifier for such cases.

extension View {
    @ViewBuilder
    func `if`<Content: View>(_ condition: Bool, content: (Self) -> Content) -> some View {
        if condition {
            content(self)
        }
        else {
            self
        }
    }
}

Other glitches I’ve seen were the titles of the navigation bar. Sometimes, the previous title doesn’t disappear, it’s merged with the current title and you can’t read anything. Another thing is the animation of text views, sometimes there are ellipsis (…) that appear and never go away. To solve this, you would need to explicitly disable animation of the text views.

At the moment, the app is more stable and maybe once in few days there’s a crash with no more details on how to fix it. For example just this: AttributeGraph: 0x1cb4e3000 + 205760, and in the stack trace, there’s a call to main method of the app and crash in GraphicsServices.

Let’s hope such issues will not appear in newer SwiftUI versions. While reading online some posts why SwiftUI is not ready for production, there were other things mentioned, like missing some standard UI components and not having a navigation stack info. For the first one, you can easily wrap the UIKit equivalents, or even build your own custom components. For the navigation stack, since Apple doesn’t provide it even in iOS 14, maybe it’s time to re-think how we design navigation in our apps.

Conclusion

SwiftUI can be used for production, but you need to have in mind the glitches mentioned above. I’m pretty sure they would be solved over time, maybe some of them are already fixed in iOS 14. Yes, you will have occasional crash and a one-star rating, but that’s nothing compared to having constant crashes for few hours because you’ve included a third-party SDK (you know which one).

Therefore, if you start a new project now, even more complex one, I believe SwiftUI is the way to go. I even did that for a bigger project at work, started a month ago. And so far, so good.

What are your thoughts on the topic, is SwiftUI ready for production?

You can also check out my SwiftUI-powered puzzle apps, Soccer Puzzles and Basketball Puzzles, and find some more SwiftUI bugs. 🙂

1 Comment

Leave a comment