2つのアプリを書き直して見えてきたSwiftUIのよさとつらみポイント

Mercari Advent Calendar 2020 の18日目は、メルカリ Client Architect チーム/iOS Engineer の kagemiku がお送りします!普段はiOSアプリを書きつつ、iOS開発に関する基盤を整えたり改善したりといったことをやっています💪

はじめに

突然ですが、みなさんはSwiftUIを触ったことってありますか? WWDC19にて突如SwiftUIが発表されてから1年半ほど。筆者はMercari Tech ConfアプリをSwiftUIで書き直したり、社内ハッカソンであるHackweekにて、MercariアプリをSwiftUIで書き直したりなどしてきました。また、業務でも実験的な意味合いの元少しだけ触っています。

本記事では、そういった筆者の経験を通して改めて見えてきた、SwiftUIの持つよさとつらみポイントをざっと書き出してみようと思います。まだまだUIKitが主流ではありますが、今後アプリを一から作る際に、SwiftUIも選択肢の一つとなることが多くなってくるのかなと思います。そういった際の参考になればいいなと思います🙏

具体的には次のようなことを書いております。

  • よさポイント
    • Declarative Styleによる見通しの良さ
    • Storyboard/XIBとの完全なバイバイ
    • Declarative StyleとSingle Source of Truthとの相性の良さ
  • つらみポイント
    • UIKitでできることがSwiftUIで全てできるわけではない
    • 知見やベストプラクティスがまだ少ない

よさポイント

1. Declarative Styleによる見通しの良さ👀

良さポイントとしてまず言えることは、Framework全体がDeclarative Style(宣言的なスタイル)で設計されていることによる見通しの良さです。例えば、次のような見た目の簡素なリストUIを、UIKitとSwiftUIの両方で実装してみます。

こちらがUIKitを使った場合のコードです。Reactive系のFrameworkなどは使わず、PlainなUIKitで実装しています。

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
    }
}

対して、こちらが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)
    }
}

状態に応じて変更されるUI要素を追加する場合、大きく次の3つのことが必要です。

  • UI要素の生成と配置
  • UI要素の見た目の変更(色やサイズなど)
  • 状態(ここではdata)とUI要素の結びつけ

基本的にImperative Style(命令的なスタイル)で設計されているUIKitでは、これら3つのことをコード上の(物理的に)別々の箇所でやっています。対してSwiftUIでは、それらをすべて一箇所で行っています。必要なコード量も約1/4と少なく、物理的な見通しはもちろんのこと、ロジック的な見通しもかなり良くなっています。

「それはそう」という話ではあるのですが、これが意外と効いてきます。上記のような凝ったUIでなければ(※ここ重要)、SwiftUIで実装する際のスピード感は開発体験的にかなり良いです。SwiftUIに慣れてくるとUIKitで書くのを億劫に感じることが多くなってきました(※凝ったUIでなければ)。

2. Storyboard/XIBとの完全なバイバイ👋

UIKitを使用する場合は、UI要素を作る際に次のような選択肢があったと思います。

  1. Storyboardとコードを併用する
  2. XIBとコードを併用する
  3. コードのみ

1および2では、Git管理下にあるStoryboard/XIBのconflictで度々地獄を見ることがありました。また、UIを決定するための情報源がStoryboard/XIBとコードの2つになってしまいます。3ではそういった地獄は少ないのですが、レイアウトのためのコードなど記述量はそれなりに多くなります。

SwiftUIではStoryboardやXIBファイルといったものは使用せず、UIはコードで記述します。UIを決定するための情報源はコードただそれだけとなり、Single Source of Truth(信頼できる唯一の情報源)を保つことができます。レイアウトのためのコードも宣言的に書くことができるため、見通しが良いです。

3.Declarative StyleとSingle Source of Truthとの相性の良さ🤝

Declarative Styleで設計されているSwiftUIですが、Single Source of Truth(信頼できる唯一の情報源)となっているModelとの相性が抜群に良いです。例えば、よくあるケースとして次のようなものを考えます。

  1. 画面を表示
  2. APIリクエストを発火
  3. Loading Indicatorを表示
  4. APIレスポンスの到達
  5. Loading Indicatorの非表示
  6. APIレスポンス内容の表示

これをそのままUIKitで書こうとすると、APIリクエストを発火した時点でUIActivityIndicatorViewstartAnimating()メソッドを呼び、APIレスポンスが返ってきた時点でstopAnimating()メソッドを呼んで、のような手続きを書くことになります。対してSwiftUIでは次のように書くことができます。

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
    }
}

ここでの注目したい点はvar isRequesting: Boolの定義部分とoverlay(state.isRequesting ? ProgressView("Loading") : nil) の行です。API通信状態を保持しているstate変数から導出されたisRequestingというcomputed propertyの値によって、ProgressView表示・非表示の状態を決定しています。コードの見通しも良いですし、何よりAPI通信の状態に応じて、ProgressViewの状態を明示的に操作する必要がないです。また、通信状態に応じた isRequesting のような状態を別で用意するのではなく、あくまでもScreenState.loadStatusから導出される一つの状態として扱うことで、画面全体の状態の一貫性を保つことができます。これは状態によってViewが一意に決定されるSwiftUIにおいては大切なことです。

UIKitでも、FRPをサポートするFrameworkを使用すれば近いコードを書くことはできます。SwiftUIにおいては、その上でさらにDeclarative Styleによる恩恵を享受できるため、より挙動が予測可能でかつ理解しやすいコードとなっています。

つらみポイント

1. UIKitでできることがSwiftUIで全てできるわけではない😩

つらみポイントとしてはまず、UIKitではできるがSwiftUIではできないことがまだまだたくさんある点です。これはまだ登場して約1年半のFrameworkだということを考えると仕方ないといえば仕方ないですし、じきにその差異は少なくなっていくとは思います(頼む)。

例えば、Pull To RefreshをSwiftUIのみで実現することは現状できません。UIKitとのBridgeを実現するUIViewRepresentableUIViewControllerRepresentableを利用すれば、すなわちUIKitの助けを借りればできないことはないのですが、それでも厳しいものがあります(そもそもXXRepresentableまわりは挙動が怪しい)。Pull To Refreshなんて基本も基本ですが、結構こういったようなできないケースがあります(Keyboardイベントのハンドリングや種々のCustom Transitionなど)。

今でこそLazyVStackLazyVGridなども揃っているSwiftUIですが、WWDC20での発表があるまではそれらはありませんでした。WWDC21、WWDC22と年と重ねていくにつれ、より充実したFrameworkに(きっと)なっていくのを見守っていきましょう。

2. 知見やベストプラクティスがまだ少ない🤦‍♂️

何度か繰り返しているように、SwiftUIはまだ登場して約1年半のFrameworkです。その若さ(?)ゆえ、UIKitほど知見やベストプラクティスが揃っているわけではなく、色々と手探りで実装していく必要があります。

UIKitに関する「こういうときはこうする」という知見やベストプラクティスはある程度頭に入っている方も多いと思います。しかし、SwiftUIにおいては、それらを一から探し出す壮大な旅に出かける瞬間がUIKitよりも高い頻度で出てきます。それはそれで面白いのですが、端的に言うと疲れます😇

SwiftUIを用いたアプリ開発をスムーズに行うには

これまで述べたように、SwiftUIは良い点はありつつもまだまだかゆいところに手が届かないFrameworkです。その上で、SwiftUIを用いたアプリ開発をスムーズに行うには、いろいろと諦めることが必要になってきます。

SwiftUIでできること・できないこと、得意なこと・苦手なことを把握することはマストであると言えます。その上で、PM・Designerとも足並みを揃え、SwiftUIで作ることを念頭に置いたアプリであれば、かなり生産性高く実装していけるのではと思います。

おわりに

本記事では、SwiftUIのよさとつらみポイントをざっとあげてみました。良くも悪くも新しいFrameworkであり、これからのAppleによる進化に期待しております。

これは個人的な所感ですが、今後AppleはますますSwiftUIへ舵を振り切っていくと思っています。例えば、iOS14の機能であるWidgetはSwiftUIで開発することがマストになっています。このようなケースは今後増えていくのではないでしょうか。Apple Siliconなども登場し、よりシームレスなアプリ開発・利用体験が得られるようになってきています。SwiftUIで書いたコードがほぼそのままで全てのApple Device上で動かすことのできる未来も、そう遠くないのではと思っています。


メルカリではミッション・バリューに共感できるiOS Engineerを募集しています。一緒に働ける仲間をめちゃくちゃお待ちしております!

明日のMercari Advent Calendar 2020執筆担当は、 Engineering Officeチームの hisahiko さんです。引き続きお楽しみください🎉