clusterのiOS開発を加速するためにやっていること

こんにちは!クラスター社でSoftware Engineerをしているizumiです。
今回はclusterのiOS開発についての話です。

メタバースプラットフォーム clusterは2022年2月「ワールドクラフト」という誰でも簡単に3D空間内のワールドを作成できる機能をリリースしました🎉

ワールドを作成するコアとなる体験についてはUnityで作られていますが、ワールドを作るまでの導線部分や作ったワールドのタイトルやサムネイル設定、公開したワールドの管理などはiOSのネイティブコード(Swift)で書かれており、ワールドクラフト機能のリリースに合わせて10以上の画面が増えました!
今回はそのような大量の画面を高速で開発するために取り組んでいることをご紹介します。

xctemplateによるボイラープレート生成

clusterのiOSアプリではMVVMアーキテクチャを採用しています。
また、画面遷移には各画面間を繋ぐためのCoordinatorを画面毎に生成しており、各画面のレイアウトはstoryboardを用いてレイアウトしています。
そのため、1つの画面を追加するのに最低でもViewController / ViewModel / Coordinator / storyboardの4ファイルの追加が必要になります。 

このような新規画面追加時に必要となるボイラープレートをまとめて生成できるxctemplateを用意しています。
xctemplateを用いることで、各画面で共通して利用しているファイルが全て初期化処理などの共通処理も予め記述された状態で生成されます。
これによってファイル生成や共通処理記述にかかる手間を省けるメリットがあります。

ViewModelに関してはテストコードもテンプレートで生成することでテストを追加していく意識を高めています。

ViewState管理 / Viewの共通利用

各画面で下記のようなViewStateを保持してStateに応じた表示を行うよう実装を統一しています。

enum ViewState {
  case processing // 通信中
  case idle(Content) // コンテンツ表示中
  case empty // コンテンツが0件
  case failure // エラー
}

これらの状態をViewModelからViewにbindし、View側が状態に応じた表示を行うようにしています。

Viewの実装例

indicator.isHidden = true
emptyView.isHidden = true
errorView.isHidden = true

switch state {
    case .processing:
        indicator.isHidden = false
    case .idle(let contents):
        // コンテンツ表示
        var snapshot = NSDiffableDataSourceSnapshot<Int, MyWorldListContent>()
        snapshot.appendSections([0])
        snapshot.appendItems(contents, toSection: 0)
        dataSource.apply(snapshot)
    case .empty:
        emptyView.isHidden = false
    case .failure:
        errorView.isHidden = false
}

ViewStateの遷移

状態の実装が統一されていることで、各画面ではそれぞれ本質的な部分の実装に注力することができます。

また、processing, empty, failureについては各画面で共通のレイアウトを利用することにより開発工数を削減しています。

EmptyState / ErrorStateのレイアウト例

CompositionalLayout / DiffableDataSources

clusterでは様々な画面でCollectionViewを利用していますが、基本的にどの画面でもCompositionalLayout / DiffableDataSourcesを採用することでレイアウト、データセットの方法の記述を統一しています。

最近では対応OSバージョンもiOS14以上にアップデートしたため以下のようなシンプルなListのみ表示する場合でもTableViewではなくCollectionViewを採用し、CompositionalLayout.Listを用いるようにしています。

CompositionalLayout.Listを使うとこれだけでSelf-SizingなListのレイアウトが組めます。簡単ですね。

var layoutConfig = UICollectionLayoutListConfiguration(appearance: .plain)
layoutConfig.showsSeparators = false
let layout = UICollectionViewCompositionalLayout.list(using: layoutConfig)
collectionView.collectionViewLayout = layout

CompositionalLayout.Listについて詳しくはWWDC2020のこちらのセッションで解説されています。

TableViewではなくCollectionViewを利用していることでListをGridに変更したい、一番上の行にカルーセルのレイアウトを追加したい、といった仕様の変更や追加に対しても柔軟に対応できるようになるメリットがあります。

SwiftUI

SwiftUIの採用も徐々に進めていて、UI構築やUIのコードレビューの開発加速に役立てています。
先述の通り画面全体はUIKitで組んでいて、内部のCollectionViewCellなどをUIHostingViewControllerを用いてSwiftUIで組むケースが多いです。

SwiftUIの具体的な活用事例については先日行われたREALITYさんとの合同勉強会でも紹介しているので。よろしければイベントレポートも見てみてください!

xcodesによるXcodeバージョンの固定

iOSにcommitしているメンバーの中には、UnityやAndroidなどの他領域との兼業を行っているメンバーもいます。そのため、長期間他領域を触っていて久しぶりにiOSに戻ってきたら使っているXcodeバージョンが変わっている。なんてことがあります。

そのような場合にも統一したXcodeバージョンで開発が行えるようxcodesの使用を推奨しています。

リポジトリのrootディレクトリに現在利用中のXcodeバージョンを記載した .xcodeversion というファイルを置いていて、このファイルを参照して該当バージョンのxcodeをinstall / selectするようなシェルスクリプトを用意しています。

この仕組みによってCI/CD環境でXcodeバージョンを変更する場合もブランチ毎に違うXcodeバージョンでのbuildが簡単にできます。
(clusterのCI/CD環境はDesktopアプリやVRアプリも同時にbuild可能にするためや、Unity as a Libraryを利用するために環境整備の自由度が高いJenkinsを採用しています。)

おわりに

さて、今回はclusterのiOS開発で取り組んでいる事について書いてみました。

今後は async / awaitを導入してコードをよりシンプルに記述することや、モジュール分割してビルド時間の短縮を行うことなど、開発がより加速するための仕組みを取り入れていきたいと考えています。

また、クラスター社では各職種絶賛採用強化中なので興味のある方は是非お話だけでも聞きに来てください!
メタバースというとUnityやゲーム開発っぽい側面が強い印象を持たれがちですが、このように普通のモバイル開発もやっているのでiOSエンジニアの方のご応募もお待ちしております!!