Retty Tech Blog

実名口コミグルメサービスRettyのエンジニアによるTech Blogです。プロダクト開発にまつわるナレッジをアウトプットして、世の中がHappyになっていくようなコンテンツを発信します。

RettyのiOSアプリにSwiftUIを導入した話

f:id:rettydev:20210405003449j:plain

はじめに

 Retty株式会社ではらみチーム (アプリチーム)に所属し、主にAndroid, iOSアプリの開発を主業務としている山田です。

 WWDC2019でSwiftUIが発表されてもう少しで二年となります。発表から時間が経過し、SwiftUIを取り扱うことのできるiOS13以降のシェアも大きくなっている中、そろそろ採用を検討しているプロジェクトも多いのではないか?と感じています。 Rettyにおいても、諸々の環境が整ってきたこともあり、そろそろ導入していきたいよね、という声が上がってきました。その後に導入試験やチームでの話し合い等検討を重ねた結果、弊社のアプリ開発でもSwiftUIを採用することになりました。この際に得た学びや辛みを本記事を通して共有します。

SwiftUIとは 🤔

 SwiftUIとは、AppleがWWDC2019で発表した新しいUIフレームワークです🐣。これまで手続き的にUI周りの実装を記述してきたUIKitとは異なり、宣言的にUIを記述できることを特徴としています。

developer.apple.com

「宣言的」とは簡単に説明すると、あるべき形を定義するとそのようになるスタイルです。これまでの手続き的な方法は、デザイン仕様書に記述されているあるべき形を実現するために、各種メソッドやプロパティを使いこなし、手動で描画処理の詳細の実装を行っていました。宣言的に記述できるSwiftUIでは、あるべき形をフレームワークに伝えるだけでそのようになるように描画が行われ、詳細を細かに実装する必要がありません。そのため、よりシンプルかつ楽にUIを記述できるようになります 👍

宣言的に記述できるだけでなく、フレームワークによるデータに基づいた差分更新により、パフォーマンスの高いUIをシンプルに実現可能、といった特徴を持ちます。 これまでのUIKitを使った手続き的な実装だと、データの更新をViewに反映させる処理はアプリ開発者の責任で行う必要があり、自由度こそ高いものでした。しかし、無闇に描画更新を行ってしまうようなお行儀の悪い実装も容易に行うことが可能だったため、そのようなことが起こらないように細心の注意を払って実装を行う必要がありストレスを感じる部分がありました😓

f:id:rettydev:20210402185143p:plain:w300
自由度の高さは誤った実装をも容易に許容することになります

SwiftUIを採用することで、描画処理の詳細はフレームワークの責務となり、開発者は意識する必要がなくなります。これにより、このような悩みから開発者は開放され、デザインやロジックの実装など、業務の本質により集中することができるようになります 🚀

導入までの流れ

 SwiftUIの良さに着目すると今すぐにでも導入したいと感じるところですが、もちろんのこと、簡単に導入することはできません。もしかしたらSwiftUIではこれまで作ってきたようなデザインを実現できないかもしれないですし、癖が強くて思ったよりも生産性向上に役立たない可能性もあります。そのため、良く評価しないままに表面的な理解のみで導入を決定してしまえば、未来のチームを苦しめることになってしまいます 😢。そうならないためにも、まずは元々リファクタしたかったグローバル版Rettyの検索結果画面を試験的にSwiftUIでリプレイスし、その結果や過程をチームで評価し、採用するかどうかや、Rettyにとって最適な形での導入方法を模索することにしました。

f:id:rettydev:20210405184219g:plain:w300
リプレイス対象となるグローバル版Rettyの検索結果画面。この動画ではすでにSwiftUIに置き換え済みの動作をお見せしています

試験導入してみて良かったこと😆

UIKitよりもAPIが直感的

これは筆者がUIKitにそこまで深く精通していないために感じた部分かもしれませんが、APIがUIKitよりも直感で、より容易に目的のUIを組み上げることができるようになったと感じています。例えばリスト状のUIを表現する場合、SwiftUIではListを利用し、この子ビューとして表示したい要素を繰り返し生成するだけで完成するため、以下のコード例のように数行で完結するためとても楽です。

struct ContentView: View {
    var body: some View {
        List(1 ..< 100) {
            Text(String($0))
        }
    }
}

一方でUIKitの場合、UITableViewControllerを用意し、各種ライフサイクルに合わせたメソッドを実装し、諸々の設定を明示的に行い……と、記述しなければならない事が多めになります。

final class TableViewController: UITableViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
    }

    override func tableView(_: UITableView,
                            numberOfRowsInSection _: Int) -> Int {
        return 100
    }
    
    override func numberOfSections(in _: UITableView) -> Int {
        return 1
    }

    override func tableView(_: UITableView,
                            cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = UITableViewCell(style: .subtitle, reuseIdentifier: nil)
        let label = UILabel(frame: CGRect(x: 0, y: 0, width: 30, height: 30))
        label.text = String(indexPath.row)
        cell.addSubview(label)
        return cell
    }
}

フロントエンドアーキテクチャが自動的にMVVMに統一され、一貫性UP ⬆️

フロントエンドアーキテクチャをどう統一するか、というのはアプリ開発者の悩みの種かと思います。一度定めたとしても、各開発者個人の理解の差から、気をつけていないとだんだんとブレが発生してきたりもします。SwiftUIでは、フレームワークが描画処理を担っている都合上、データの更新に合わせてオブザーバブルにViewを更新するMVVM以外に取り得る選択肢がなく、一貫性を保ちやすいです。

パフォーマンスが良い 🚄

冒頭でも解説した通り、フレームワーク側でいい感じに差分更新をしてくれるため、扱うデータ量が増えたとしてもパフォーマンスが良い状態を維持しやすいです。これまで、UIKitでは気をつけていないと余計な描画処理まで実行してしまい、パフォーマンスが劣化してしまうような事が多くありましたが、SwiftUIではそういった心配から解放されます。

既存UIに部分的にSwiftUIを導入しやすい

UIHostingControllerを利用する事で、容易にSwiftUIを既存のUIKitで作られたViewの中で利用する事ができます。このため、部分部分で必要な箇所でのみSwiftUIを利用するという選択ができるようになります。 実際にこの機能を利用して、UIKitの一部を入れ替える形で画面のリプレイスを行ったのですが、特に苦労なく扱う事ができ、とても快適でした。

試験導入してみて課題と感じたこと 😢

内部で自身の描画を更新するUIKit製のViewをSwiftUIでうまく利用できない

SwiftUIには、UIViewRepresentableというProtocolを利用する事で、UIKitにより作成された既存のViewをSwiftUIで利用できる機能があります。

しかし、この機能によって「タップなどのイベントで自身で自身の描画を更新するUIKit製View」をSwiftUIで利用しようとしたところ、うまく描画更新が行われませんでした。

SwiftUIの性質を考えるとこの動作は納得のいくものとなっており、SwiftUIが知っている範囲のデータに基づいて描画更新をするかどうか決めているため、知らない箇所に存在するデータに依存しているViewについては描画を更新してくれないことになります。 Rettyの既存のViewについては自身で自身の描画を管理するものが結構多く存在していたため、これによりこれまで出来ていた既存Viewの使い回しによる工数削減は望めないことになりました。

OSバージョン間のAPIの違いが激しい

iOS13, iOS14では利用されるSwiftUIのバージョンもそれぞれ1.0, 2.0と異なるものとなっています。このバージョンの違いによるAPI,機能の違いがUIKitよりも大きく、両方をサポートしようとするとOSバージョン依存の分岐がかなり増えます。例として、リスト状のUIを実装する際にはiOS13では Listを利用しますが、iOS14では同様のUIを実装できるLazyVStack, LazyHStackというViewも追加され、iOS13から存在していたListは挙動が一部変更されています。そのため、ユースケースによってはiOS13ではList、iOS14ではLazyVStack, LazyHStackのようなな出し分けを実装して工夫をする必要があります。現状これがバージョン二つなのでまだ良いのですが、iOS15が出た時にも同じ調子だと大変そうだと感じているところです。

iOS13.0と以降のバージョンとで挙動の差異が大きい

前項でメジャーバージョンの差異による違いを上げましたが、マイナーバージョンによる挙動の差異もそれなりにあり、特にiOS13.0では違いが多く、このバージョン向けにSwiftUIを利用するのは厳しいという評価を下さざるを得ませんでした。利用してみて発見した挙動差異はいくつかありますが、例として、ある程度複雑なレイアウトを組むとSpacerが意図した挙動をしないと言うものがありました。例えば、以下のようなViewがあるとします。

struct ContentView: View {
    var body: some View {
        HStack {
            VStack {
                Image(systemName: "hand.thumbsup.fill")
                let loremIpsum = "Lorem ipsum dolor sit amet, consectetur adipiscing elit..."
                Text(loremIpsum).lineLimit(1)
            }
            Spacer()
        }
    }
}

ImageTextが縦に配置され、Textが一行表示され、残りの領域をSpacerが横方向に埋めてくれる、と言う挙動を想定してこのようなViewを組んだ場合、iOS13.0でのみ以下のように意図した通りにはなりません。

f:id:rettydev:20210405190945p:plain:w800
iOS13.0でのみ意図したレイアウトにならない

恐らくiOS13.0でのみレイアウトを行う際のデフォルトのPriorityがiOS13.1以降と異なっている事が原因のようで、VStackに適当に大きめのPriorityをlayoutPriorityで与える事で解決しますが、iOS13.0を考慮して、細かくlayoutPriorityを設定していくのはかなり辛いですし、確認漏れによるレイアウト崩れが容易に起こり得そうで扱いが難しいと感じるところがあります。

iOSらしさから離れたデザインを実装しようとすると結構大変な印象

SwiftUIでは、View間のspaceやスタイルなど、各種見た目に関するパラメータのデフォルト値が「iOSらしいデザイン」を実現するものとなり、そのままだとiOS準拠な見た目になります。例えばListは、「スクロールに合わせて子Viewを遅延描画可能なView」ではなく、あくまで「iOSらしいリスト」となり、そのまま使うと丁度iOSの設定画面のような仕切り線付きの見た目となり、工夫しないとこの線を消す事ができません。こういった設計思想はListだけでなく、SwiftUIのView全体に感じる部分で、iOSらしい見た目から乖離したUIを実現しようとすると、難易度・記述量が共に上昇します。アプリのデザインは出来るだけOSに従った方がユーザーさんにとっても学習コストが減り親切なので、一概にこれがデメリットと評することもできないのですが、デザインによっては思ったよりも工数がかかってしまうことは認識しておいた方が良さそうです。

試験導入の結果を踏まえて 💁‍♂️

 試験導入の結果、やはりメリットもあればデメリットもあり、特にバージョン間の挙動差異が激しい件については今後の保守運用にかかるコストが増加しそうで、よく考えて導入する必要がありそう、という事がわかりました。とはいえ、それを補ってあまりあるほど開発効率の向上につながるのではないか、とも考えており、とりあえずのところは導入してみてもよいのではないか、という結論を出しています。

 以上の結果を踏まえた議論の結果、Rettyのアプリ開発チームでは「新規実装部分についてはSwiftUIでの実装推奨」という方針で導入する事が決定しました 🎉 まだまだ全面的に切り替えていくには不安が残る部分があるものの、採用例も増えてきており、今後のトレンドとしてSwiftUIを扱えるようになっている必要があるという事で、可能なところでは挑戦していきましょう 🚀 というスタンスとなっています。

終わりに

 RettyでのSwiftUI導入に関して得られた学びを共有させていただきました。まだ不安が残る箇所もありますが、筆者個人としてはこれまでよりも効率的にUIを実装できるSwiftUIに希望を感じています。SwiftUIが成熟し、UIKitに取って代わり、悩み事が減り、開発作業をより楽しく行えるようになるその日が今からでも待ち遠しい限りです 🚀