チームで挑む、ZOZOTOWN iOSでのSwiftUI導入実践話

チームで挑む、ZOZOTOWN iOSでのSwiftUI導入実践話

こんにちは、ZOZOTOWN開発1部iOSブロックの荻野です(@juginon)。

WWDC19でSwiftUIが発表されてから今年で5年になりますが、みなさんの携わっているプロジェクトではSwiftUIを使っていますか。ZOZOTOWN iOSチームでは、2023年11月にリリースしたアイテムレビュー機能1へ向けSwiftUIを積極的に導入する方針を定め、新規画面及びUI要素ではSwiftUIでの実装に取り組みました。

巨大なプロジェクトであるZOZOTOWN iOSにSwiftUIをどのように導入していったのか? 本記事では、実際にプロダクトコードに導入したことで見えた問題点、そこで得た知見を踏まえた上でのSwiftUIの導入方針についてご紹介します。

ZOZOTOWN iOSへのSwiftUI導入の背景

ZOZOTOWN iOSへSwiftUIを導入した主な目的は、チーム全体でSwiftUIへの知見を増やすためです。今までもZOZOTOWN iOSにSwiftUIを導入した前例はありましたが、メンバー個人での挑戦に留まっていました。取り組んだメンバー同士で振り返ってみて、巨大で複雑なZOZOTOWN iOSのコードの上でSwiftUIの知見を増やすには、メンバー個人ではなくメンバー全員で取り組んだ方が良いと判断しました。そこで、ZOZOTOWN iOSにSwiftUIを導入するチームとしての方針を決めることにしました。SwiftUIへの挑戦や実際に発生した問題の共有をチームメンバー全員で取り組むことで、チームとして実践的な知見を増やせると期待しました。

また副次的な効果として、Previews in Xcodeを積極的に使うことでデザイン仕様の実装漏れを防止する期待もあります。機能要件の複雑さゆえにデザイン仕様の見落としが少なからずあるので、デザイン仕様から考えられる複数のユースケースをPreviewsで確認しながら実装できると仕様の見落としに気づきやすくなるでしょう。

これらの背景から、ZOZOTOWN iOSではSwiftUIの導入が検討されるようになりました。まずは導入方針を決めるにあたってどのような意思決定がされたのかについてご紹介します。

旧方針:限定的な範囲で小さく導入する

導入の方針を決める際は、画面単位で一気にSwiftUIを導入していくのか、コンポーネントにわけて小さく導入していくのか、大きく分けて2つの選択肢を考えました。チームの議論では以下のような意見が出ました。

  • 一気に画面全体をSwiftUI化することはコードの規模や時間的に難しい

    • ZOZOTOWN iOSでは複数の案件が同時並行で走っており、影響範囲が大きい改修を入れるのは現実的でない。
    • もし新規で画面を作る場合でも、問題が発生した際にUIKitの実装へフォールバックさせる判断をしたときのダメージが大きい。
  • ユーザーに価値を届けることを最優先にするが、技術的な挑戦もないがしろにするべきではない

    • まずは小さく始めて導入に際しての懸念点を探し、チーム内で知見を増やすべき。

上記のように、最初は限定的な範囲で小さくSwiftUIを導入し、徐々にその適用範囲を広げていくことでチームのSwiftUIに対する理解と経験を速いサイクルで段階的に深めていこう、と決まりました。

具体的には次のアクションに沿ってSwiftUIをチームで導入することにしました。

UIViewで書いている部分はSwiftUIで実装する

UIKitで実装することを考えたとき、UIViewとして書くであろう部分はSwiftUIのViewで実装します。具体的には以下のような例が考えられます。

  • UIViewController内で表示するビューをSwiftUIのViewで実装する
  • UIViewController内で表示するビューはUIViewのままで、ビューを構成する各コンポーネントをSwiftUIのViewで実装する

UIViewControllerの中でSwiftUIのViewを表示するときは UIHostingController を使用します。親のUIViewControllerにSwiftUIのViewを持つUIViewControllerを追加する形で表示します。

とはいえ、SwiftUIの標準APIだけでは実現できないケースもありえることはチームでも話に上がっていたため、次のアクションも合わせて決めました。

SwiftUIの標準APIで実現できないことがあるケースではUIKitを使用する

例として、リスト表示におけるSwiftUIとUIKitの実装を考えます。ZOZOTOWN iOSのUIデザインはSwiftUIのGridでは実装が難しく、UICollectionViewでないと表現しづらい複雑なレイアウトやデザインになっていることもあります。

この例に限らず、現状ZOZOTOWN iOSにおける様々なデザイン仕様に対してSwiftUIの標準APIだけで実現するには難しい状況です。

そのため、SwiftUIだけでは実現できない画面やコンポーネントに関してはUIViewを利用します。

実際に導入した結果うまくいかなかった

アイテムレビュー機能では、上記の方針をもとに実際にいくつかの画面でSwiftUIのビューを作成しました。

最初に、新規作成されたレビュー詳細画面におけるSwiftUIでの実装予定と最終的なリリース時点での実装を比較してみましょう。図中の赤枠部分がSwiftUIでの実装です。

実装予定では4つのコンポーネントをSwiftUIで実装する計画を立てていましたが、実際に計画通り実装できたのは上2つの赤枠のコンポーネントのみとなりました。

実装予定 リリース時点での実装
SwiftUIで実装予定だったビュー リリース時点でのSwiftUIで実装されたビュー

計画通りにSwiftUIで実装できなかったコンポーネントは次の3つです。1つ目はユーザーの投稿を表示しているセル、2つ目はユーザーのリアクションを表示するビュー、3つ目は星評価のビューです。

技術的な課題の複雑さとプロジェクトのスケジュールの両方を考慮し、1つ目と2つ目についてはやむを得ずUIKitの実装へと戻しました。3つ目の星評価のビューについては不具合の発生条件があったため、原因を調査して対応した結果SwiftUIのまま実装できました。

この星評価のビューの対応について詳しくご紹介します。星評価のビューは以下の図の赤枠で使用されています。

星評価

赤枠のうち上から2つのビューは正しく表示されましたが、一番下の赤枠のみ星が表示されなくなる不具合を発見しました。

なぜ他の部分ではうまく表示できてこの箇所のみうまく表示できないのか、詳しく原因を調査することにしました。

左図: レビューセルの構成 右図: 不具合のイメージ

この画面は UICollectionView がベースとなっており、そこに各セクションのビューを表示しています。

アイテムレビューのユーザー投稿は UICollectionViewCell で実装されています。セルの中で表示している星評価はSwiftUIのViewで作成したため、 UIHostingController を使用して表示していました。

このとき、以下の特定の状況において星評価のビュー自体が表示されませんでした。

  • レビューの投稿一覧のうち、上から2番目のレビューの星のみが表示されない
  • iOS 14系、 iOS 16系では再現せず、iOS 15系でのみ再現が確認できる
  • ファーストビューに表示されるセルの位置関係によって表示されない場合がある

どう解決したか

不具合が起きているビューの特徴として、UICollectionViewCell の内部で表示していることに着目しました。

同様の問題について言及しているApple Developer Forumsを確認しました。公式の回答によると、UIHostingControllerUICollectionViewCell の内部での使用はサポートされてないようです。

Embedding a UIHostingController inside of cells is not officially supported, and you may run into various issues doing this.

本来であればこの段階でUIKitでの実装へ戻すアクションを取るべきです。

しかし、チーム内で調査を進めていった結果、今回の問題は safeAreaInsets がSwiftUIによって意図せず設定され、レイアウト崩れが発生していることが原因であるとわかりました。

ビュー自体は正常に追加されていたものの、safeAreaInsets が設定されていたことで星を描画するサイズを確保できていなかったようです。safeAreaInsetsをゼロにすることで解決できました。

表示されない場合 表示される場合
表示されない場合の星評価のビューレイアウト 表示される場合の星評価のビューレイアウト

iOS 16.4以降の場合、 UIHostingControllersafeAreaRegions2remove(.all) することでこの問題を解決できました。しかし、アイテムレビューリリース時のZOZOTOWN iOSはiOS 14以降をサポート対象OSとしていたため、iOS 16.4未満でも safeAreaInsets をゼロにする必要がありました。

iOS 16.4未満の場合には、独自で作成した SafeAreaRemovedHostingController を使用しました。SafeAreaRemovedHostingControllersafeAreaInsets をゼロにしたUIViewを持つ UIHostingController です。

import SwiftUI
import UIKit

final class SafeAreaRemovedHostingController<View: SwiftUI.View>: UIHostingController<View> {
    private class WrapperView: UIView {
        override var safeAreaInsets: UIEdgeInsets {
            .zero
        }
    }

    private let wrapperView = WrapperView()

    override func loadView() {
        super.loadView()
        view.backgroundColor = .clear
        view.translatesAutoresizingMaskIntoConstraints = false
        wrapperView.addSubview(view)
        NSLayoutConstraint.activate([
            wrapperView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            wrapperView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            wrapperView.topAnchor.constraint(equalTo: view.topAnchor),
            wrapperView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
        ])
        view = wrapperView
    }
}

SafeAreaRemovedHostingController を用いて以下のようにOSバージョンによって出し分けました。これによってZOZOTOWN iOSでサポートしているすべてのOSバージョンで星評価のビューを問題なく表示できました。

let starView: UIView = {
    if #available(iOS 16.4, *) {
        let hostingController = UIHostingController(rootView: starRatingView)
        hostingController.safeAreaRegions.remove(.all)
        return hostingController.view
    } else {
        return SafeAreaRemovedHostingController(rootView: starRatingView).view
    }
}()

UICollectionViewUITableView の中でSwiftUIを表示する仕組みとしては、WWDC22で UIHostingConfiguration が紹介されています3

UIHostingConfiguration の導入はiOS 16から可能となり、より簡潔に不具合なく実装できることが期待されます。ZOZOTOWN iOSのサポートバージョンがiOS 16以上へと上がったタイミングで上記の出し分けは消え、 UIHostingConfiguration へと移行する予定です。

新方針:小ささの粒度を柔軟に変えながら導入する

小さく導入するという旧方針に従って実際にSwiftUIで実装した結果、場合によって小ささの粒度を柔軟に変える必要がありました。これを踏まえて、ZOZOTOWN iOSにおけるSwiftUI導入のアクションは以下のように変更しました。

  • UIViewのサブビューにコンポーネントが複数ある場合、コンポーネントはSwiftUIかUIKitのどちらかに統一する
  • UIHostingConfiguration を使えるようになるまでは UICollectionViewCellUITableViewCell でSwiftUIを使わない
  • SwiftUIで実装する際には、UIKitの実装へと戻す可能性も考えたリソース配分・スケジューリングを行う

すでに上記のアクションを基に画面単位でSwiftUIへのリファクタリングをおこなった部分もあり、これからもこの方針でSwiftUIの導入を進めていきます。

まとめ

本記事ではZOZOTOWN iOSにSwiftUIを小さく導入するに至った経緯、実際に導入したことで見えてきた問題点、そこで得た知見を踏まえた上でのSwiftUIの導入方針をご紹介しました。

本記事で具体例として挙げたような問題は他にも存在する可能性がありますが、チームとしては都度調査し、SwiftUIとの共存を目指しています。今回のアイテムレビュー機能で小さく始めた経験は、チームにとって重要な知見を得る機会となりました。

個人開発なら時間をかけさえすれば解決できるような問題も、実際に仕事として進めていく中で直面すると臨機応変に対応しなければなりません。この経験はチームの文化としてSwiftUIを導入しようとしなければわからなかったことで、個人的には貴重な経験でした。

そういった短い時間での意思決定と技術的挑戦のバランス感を今後も養っていき、よりお客様に価値を提供できるといいなと思っています。

ZOZOTOWN iOSチームは今回の例に限らず、技術導入に積極的に挑戦しつつも、常にお客様に価値を提供することを最優先に考えています。

ZOZOTOWN iOSチームでは、そのような価値観を持っている方と一緒にサービスを作り上げたいと思っています。ご興味のある方は、以下のリンクからぜひご応募ください!

hrmos.co

カテゴリー