SwiftUI’s strengths and weaknesses by rewriting two apps

* This article is a translation of the Japanese article written on Deember 18, 2020.

Day 18 of the Mercari Advent Calendar 2020 is written by iOS Engineer kagemiku from the Mercari Client Architect Team. I’m usually writing iOS apps and improving the infrastructure of iOS development💪.

Introduction

All of a sudden, have you ever used SwiftUI? It’s been about a year and a half since Apple suddenly announced SwiftUI at WWDC19. I rebuilt the Mercari Tech Conf app with SwiftUI, and also rebuilt the Mercari app with SwiftUI at Hackweek, our internal hackathon. I’ve also been experimenting with SwiftUI a bit at work as well, in an experimental sense.

In this article, I’d like to write down some of the pros and cons of SwiftUI that I’ve come to know through my experience so far. UIKit is still the mainstream, but I think SwiftUI will be one of the popular options when developing apps from scratch in the future. I hope this article will be helpful to you in such a case. 🙏

Specifically, I have written the following.

  • Pros
    • Good visibility with Declarative Style
    • A complete bye-bye with Storyboard/XIB
    • Declarative Style and Single Source of Truth are a good match
  • Cons
    • Not everything that UIKit can do can be done with SwiftUI
    • Knowledge and best practices are still scarce

Pros

1. Good visibility with Declarative Style👀

The first good thing to say is that the entire Framework is designed in Declarative Style, which improves the visibility of the programming code. For example, let’s implement a simple list UI that looks like this in both UIKit and SwiftUI.

The following is the code for using UIKit, which is implemented plainly in UIKit without using Reactive Framework.

final class TableViewController: UIViewController {
    private let data = Array(0..<20)

    private lazy var tableView: UITableView = {
        let view = UITableView(frame: .zero, style: .plain)
        view.translatesAutoresizingMaskIntoConstraints = false
        return view
    }()

    override func viewDidLoad() {
        super.viewDidLoad()

        navigationItem.title = "TableView"
        tableView.dataSource = self
        view.addSubview(tableView)

        NSLayoutConstraint.activate([
            tableView.topAnchor.constraint(equalTo: view.topAnchor),
            tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
        ])
    }
}

extension TableViewController: UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        data.count
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell: UITableViewCell
        if let dequeuedCell = tableView.dequeueReusableCell(withIdentifier: "TableViewCell") {
            cell = dequeuedCell
        } else {
            cell = UITableViewCell(style: .default, reuseIdentifier: "TableViewCell")
        }
        cell.textLabel?.text = String(data[indexPath.row])

        return cell
    }
}

In contrast to the above, the following is the code using SwiftUI.

struct TableView: View {
    @State private var data = Array(0..<20)

    var body: some View {
        List(data, id: \.self) { value in
            Text(String(value))
        }
        .navigationTitle("TableView")
        .navigationBarTitleDisplayMode(.inline)
    }
}

When adding UI elements that change based on their state, there are three major things that need to be done.

  • Generating and placing UI elements
  • Changing the appearance of UI elements (color, size, etc.)
  • Associating state (data in this case) with UI elements

In UIKit, which is basically designed in Imperative Style, these three processes are done in different parts of the code. On the other hand, SwiftUI does them all in one place. The lines of code required is about a quarter less, and the physical and logical visibility is much better.

This is a bit of a "yes, that’s true", but it’s surprisingly effective. As in the example above, if we have a simple UI ( this is important), the speed of implementation in SwiftUI is quite good in terms of development experience. Once I got used to SwiftUI, I started to feel like I couldn’t be bothered to write in UIKit ( except for an elaborate UI).

2. A complete bye-bye 👋 with Storyboard/XIB

When using UIKit to create UI elements, you may have had the following options.

  1. Using Storyboard and code together
  2. Using XIB and code together
  3. Code only

In #1 and #2, I suffered many times from Storyboard/XIB conflicts under Git control. I had to consider the determined UI by 2 sources: Storyboard/XIB and code. In #3, there is less suffering, but the lines of code for layout is much larger.

SwiftUI does not use Storyboards or XIB files, but instead writes the UI in code. The code is the only source of information for determining the UI, keeping it a Single Source of Truth. The code for the layout can also be written declaratively, making the programming code easier to understand.

3. Declarative Style and Single Source of Truth are a good match 🤝

SwiftUI is designed in Declarative Style, and it works very well with Model which is a Single Source of Truth. For example, the following is a common case.

  1. Display the screen
  2. Call an API request
  3. Display Loading Indicator
  4. Received at the API response
  5. Hide Loading Indicator
  6. Display content of the API response

If we try to write this in UIKit, we will call the startAnimating() method of UIActivityIndicatorView when the API request is called, and call the stopAnimating() method when the API response is returned. In contrast, SwiftUI can be written as follows.

struct SampleView: View {
    enum LoadStatus {
        case idle
        case loading(id: Int)
        case loaded(Result<String, Error>)
    }

    struct ScreenState {
        let loadStatus = LoadStatus.idle

        // here
        var isRequesting: Bool {
            guard case .loading = loadStatus else { return false }
            return true
        }

        var response: String? {
            guard case .loaded(.success(let response)) = loadStatus else { return nil }
            return response
        }
    }

    @State private var state = ScreenState()

    var body: some View {
        VStack {
            Text("SampleView")

            if let response = state.response {
                Text("Result: \(response)")
            }
        }
        .overlay(state.isRequesting ? ProgressView("Loading") : nil)    // here
    }
}

The main points to note in this code are the definition of var isRequesting: Bool and the line of overlay(state.isRequesting ? ProgressView("Loading") : nil) . Depending on the value of the computed property isRequesting, which is derived from the state variable that holds the API communication state, the ProgressView show and hide state is determined. The code is easy to read, and above all, there is no need to explicitly manipulate the state of ProgressView according to the state of API communication. Also, instead of having a separate isRequesting state based on the communication state, we can keep the state consistent across the screen by treating it as a single state computed from ScreenState.loadStatus. It is important in SwiftUI, where the view is uniquely determined by the state.

Even in UIKit, if we use a framework that supports FRP, we can write similar code. However, in SwiftUI, we can get the benefits of Declarative Style, which makes the behavior more predictable and easier to understand.

Cons

1. Not everything that UIKit can do can be done with SwiftUI😩

The first disadvantage is that there are still many things we can do with UIKit but not with SwiftUI. This is unavoidable considering that the Framework has only been around for about a year and a half, and I expect that the differences will gradually decrease.

For example, currently, Pull To Refresh cannot be implemented only with SwiftUI. ( This information is old, Pull To Refresh in SwiftUI is introduced in iOS 15)

We can do it with the help of UIKit, such as UIViewRepresentable or UIViewControllerRepresentable, which provide Bridge with UIKit, but it’s still difficult (the behavior of methods like XXRepresentable doesn’t seem to be accurate right now). Pull To Refresh is very basic, but there are cases where it is not possible to achieve it in SwiftUI like this (such as handling Keyboard events and various Custom Transitions).

SwiftUI now has LazyVStack and LazyVGrid, but they were not available until the announcement at WWDC20. Let’s keep an eye on SwiftUI as it becomes a more complete framework with WWDC21 and WWDC22.

2. Knowledge and best practices are still scarce🤦‍

As I have repeated several times, SwiftUI is a new Framework that has only been around for about a year and a half. So SwiftUI doesn’t have as much knowledge and best practices as UIKit, and we need to explore various ways to implement it.

Many developers have a good idea of what to do and best practices for UIKit. However, in SwiftUI, we will encounter the opportunity to search for such knowledge from scratch more often than in UIKit. That’s interesting, but in other words, exhausting. 😇

How to smoothly develop apps using SwiftUI

As I have mentioned so far, SwiftUI is a Framework that has some good points but is not yet detailed enough. Knowing this, it is necessary to give up a lot of things in order to smoothly develop apps using SwiftUI.

We have to understand what we can and cannot do with SwiftUI, what SwiftUI is good or not. And if we are aligned with PMs and designers, and if the app is developed with SwiftUI in mind, I think we can implement it with high productivity by SwiftUI.

Conclusion

In this article, I have briefly listed the good and bad points about SwiftUI. It is a new framework, for better or worse, and I look forward to future evolution by Apple.

This is my personal feeling, but I believe that Apple will be shifting more and more towards SwiftUI in the future.

For example, Widget, a feature of iOS14, must be developed with SwiftUI.

I think this kind of case will increase in the future.

With Apple Silicon, the app development and usage experience is becoming more seamless.

We will soon get a future where code written in SwiftUI can be run on all Apple devices almost without modification.


Mercari is looking for iOS Engineers who share our mission and values. We are very, very excited to have you join us!

Tomorrow’s Mercari Advent Calendar 2020(in Japanese) will be written by hisahiko from the Engineering Office team. Stay tuned🎉.