🍄

LUUP iOSアプリのプロジェクト構成

2022/12/16に公開

※この記事は Luup Developers Advent Calendar 2022 の16日目の記事です。

こんにちは、iOSアプリエンジニアの茂呂(@slightair)です。

今年の10月からLuupで働いています。 入社してから日は浅いですが、LUUP iOSアプリの開発環境はモダンできれいにまとまっていたのですぐにキャッチアップでき、本筋の開発に入ることができています。 そのようなLUUP iOSアプリのプロジェクト構成について紹介させてください。

はじめに

LUUPは電動マイクロモビリティのシェアサービスです。 スマートフォンアプリを利用して最寄りの貸し出し・返却ポートを見つけ、その手続きができます。 移動に用いる車両もそうですが、アプリの使いやすさがユーザー体験に直結するものであり、日々機能開発と更新を繰り返し、サービスの向上に取り組んでいます。

LUUP iOSアプリは開発が始まってから4年くらい、現在は正社員2名、業務委託4名のiOSアプリエンジニアで開発しています。 このような開発規模ですが、規模の割にはかなり整った開発環境だな、というのが最初に受けた印象でした。 モダンなiOSアプリ開発手法をうまく取り入れて、サービス開発を続けられています。 自分は前職で社内のモバイルアプリの開発環境を横断的に整える仕事をしていましたが、特にそう感じました。

この記事では、LUUP iOSアプリのプロジェクト構成とその特徴的なところについて解説します。

SwiftPackageManagerの活用

一般的なiOSアプリのように、LUUP iOSアプリも多数のOSSライブラリ・ツールを利用して開発を進めています。 依存するライブラリーや開発ツールはすべてSwiftPackageManager(以下SwiftPM)のみで管理しています。 これについては先日公開した記事[1]でも触れました。 CocoaPodsからSwiftPMへの移行をどのように進めたかは、Luup Developers Advent Calendar 2022の19日目の記事で解説する予定です。お楽しみに。

外部ライブラリーだけでなく、LUUP iOSアプリを構成するモジュール群とそれらの依存関係についてもSwiftPMで管理しています。 アプリを実装するソースコードほぼすべてをSwiftPMのターゲット管理にすることで、Xcodeのプロジェクトファイルに含めるファイルを最小限にできます。 これはプロジェクトファイルの編集差分を最小限にすることにもつながり、チーム開発の際のコンフリクト回避にもつながっています。 モジュールの分割戦略については次節で詳しく解説します。

SwiftGenやLicensePlistといったビルド時に用いるツールもSwiftPMで導入し、XcodeのAggregate Target経由で実行しています。SwiftLintのようなBuildToolPluginが提供されているものはそれを利用しています。

設定のみを含む小さなアプリターゲット

LUUP iOSアプリは構成するモジュール群と各種設定のみを含む小さなアプリターゲットによって、ひとつのアプリに組み立てられます。具体的にはだいたい以下のような構成になっています。

- LUUP.xcworkspace - Prod/Stgプロジェクト、SwiftPMパッケージをまとめたワークスペース
- Projects/
  - Prod.xcodeproj - 一般公開アプリプロジェクト
  - Stg.xcodeproj - 開発環境アプリプロジェクト
  - 各種設定ファイル群(GoogleService-Info.plistなど)
- Package.swift
- LUUP/ - アプリモジュール
  - AppDelegate.swift
  - まだ機能単位のモジュールに分割されていないコード群
- Entities/ - モジュール
- DesignSystems/ - モジュール
- Resources/ - モジュール
- Services/ - モジュール
- UI/ - モジュール、機能単位
  - FavoritePort/
  - Profile/
  - RideHistory/
  - ...
- Tests/ - テストターゲットモジュール
- ...

ワークスペースによって一般公開用と開発環境用のふたつのプロジェクト、SwiftPMのパッケージをまとめています。 プロジェクトファイルではそれぞれ各環境に合わせた設定ファイルを参照しています。

アプリの実装はAppモジュールとして分けられており、ここにiOSアプリのエントリポイントとなるAppDelegateもおいています。 プロジェクトのアプリターゲットからこのAppモジュールへの依存をもつことで、iOSアプリとして機能しつつ、環境に合わせた設定を切り替えるだけのプロジェクト構成を実現しています。 前節にあるとおり、Appモジュールから先の依存関係はSwiftPMで定義しています。

アプリを構成するモジュール群

前節でいくつかのモジュールを出していましたが、LUUPアプリのモジュールは大きく以下のものに分類できます。代表的なモジュールを例に出しています。

- アプリモジュール
  - App - アプリ本体
  - NotificationService - NotificationServiceExtension
- コアモジュール - アプリ全体で広く利用されるような基礎となるモジュール群
  - Entities - ドメインモデル、データモデルを定義するモジュール
  - DesignSystems - UIコンポーネントや色を定義しているモジュール
  - Services - Firebaseなどへの通信やUserDefaultsの読み書き、ビジネスロジックを実装するモジュール
  - Resources - 文字列や画像リソースを定義しているモジュール、SwiftGenを利用してコード生成
  - その他
- UIモジュール - 機能単位でまとまった少数の画面を実装するモジュール群
  - お気に入りポート
  - プロフィール
  - ライド履歴
  - その他
- テストターゲットモジュール
  - Tests - ユニットテスト

LUUPアプリは以下の観点を考慮して、上記のような構成のモジュール分割を進めています。

  • モジュール間の依存を意識することで、適切に責務分割を進めたり、可視性の指定ができる
  • 機能単位での開発のしやすさを向上し、開発サイクルをより速く回せるようにする
  • ビルド時間の短縮、XcodePreviewsの動作安定が見込める

XcodePreviewsを利用する際は、アプリ本体とは別に用意した軽量なターゲットを利用して動作させています。 XcodePreviewsを動かすための必要最低限のモジュールのみ含めているので動作が安定します。

SwiftPMを用いてモジュールを定義し依存関係を構築しているので、Package.swiftとファイルの配置のみで完結し、わかりやすい構成になっています。今後モジュールの整理や再配置が必要になった場合でも、容易に変更できるでしょう。

文字列や画像リソース、色のAssetsはUIモジュール単位で分けるということはせず、1カ所に置いています。 特に画像リソースについては、UIモジュールに分割し切れていない実装やStoryboard/xibからの参照がAppモジュールにまだ残っているので、xcassetsをApp モジュールのディレクトリーに配置しています。 InterfaceBuilderでは画像リソースの出所(モジュール)を指定できないためこのようにしつつ、ソースコードからの参照はResources モジュール経由となるようにSwiftGenの設定で工夫しています。 Storyboard/xibによる問題はいくつかありますが、これらの画面実装は少しずつソースコードでの実装に置き換えようと考えているところです。

設計と実装方針

LUUPアプリはCleanArchitecture + MVPアーキテクチャで実装しています。 画面単位でViewControllerとPresenterクラスを実装し、Presenterからビジネスロジックを持つ複数のServiceクラスを呼びだす形です。 特にFirestoreからのデータ読み出しやCloud Functionsで構築したWebAPI呼び出しを多く行うアプリケーションなので、様々なServiceを定義しています。

画面構築は基本的にUIKitで行いつつ、構成するコンポーネントはSwiftUIで実装するのを試しています。 例えば、CollectionViewCellの中身をSwiftUIで実装するのはよくあるパターンです。 前述したアーキテクチャでの開発を続けたり、様々なタイプの画面遷移に対応するためこのような方針を取っています。コンポーネント単位では今後SwiftUI実装の比率が上がっていくと思います。

LUUPアプリの非同期処理は現状PromiseKitによるPromiseでの実装が多いのですが、Swift Concurrencyが使えるようになってからは積極的に利用するようにしています。 新規実装はasync/awaitで記述しつつ、既存の実装を利用する場合はPromiseからasync/awaitのAPIで使えるように変換するコードが入っています。 これらもいずれasync/awaitに置き換えていけるはずです。

おわりに

LUUPのiOSアプリプロジェクトは、機能開発をしっかりと進めつつ、開発体験の向上のためにモダンな開発手法を取り入れることをバランスよく進めることができていると思います。 今後もうまくバランスを取りながら、機能開発を進めサービス成長につなげていけたらよいなと考えています。

今後はSwiftUIやXcodePreviewの活用を進めるために、既存の実装からStoryboard/xibによる画面実装を減らす、モジュール分割を進める、async/awaitへ実装を書き換えを積極的に行っていきます。

iOSアプリのプロジェクト構成の参考になれば幸いです。 また、この記事を読んでLuupでのiOSアプリ開発にご興味をお持ちいただけたら、お声がけください。

https://recruit.luup.sc

脚注
  1. Luup iOSアプリ開発の現状と課題 ↩︎

Luup Developers Blog

Discussion