Evolution of mikan iOS 2022
これはmikan Advent Calendar 2022 2日目の記事です。
1日目は@aviciidaから「習慣化に必要なことを言語化してみた」でした。彼の習慣化スキルは本当にすごい・・!
最高の英語アプリ「mikan」のiOSアプリエンジニア、@satoshin21です!mikanにジョインしてからおおよそ1年経過し、プロジェクト構成もだいぶ様変わりしてきました。
英語アプリmikanは650万ダウンロードを超えた、たのしく英語を学ぶことができる英語アプリです。2014年にリリースされて以来、様々な機能追加や改修を日々アグレッシブにすすめています。ただ2021年ジョイン当初はDeployment TargetがiOS 10だったりと技術的な負債が溜まってきてしまい、施策に取り組む工数にもかなり影響しているような状況でした。
ここ1年でかなりモダンな構成、かつ機能追加や改修にも素早く答えられる基盤ができてきました。まだ発展途上ではあるのですが、1年の終わりというタイミングで報告も兼ねてmikan iOS 2022の進化をシェアさせていただきます。
開発体制
現在、mikan for iOSは主に私 @satoshin21 と @uk_oasis 2人のフルタイムと業務委託・副業メンバー2人で実装しています。1週間をタイムボックスとしたゆるめアジャイルでイテレーションを回しています。プロジェクト全体のバックログはNotionで管理されており、見積をするタイミングでGitHub ProjectにIssueを追加して各イテレーションの進捗の管理と振り返りを行っています。
ブランチ戦略としてはgithub-flowを採用しています。もともとあったdevelop, masterブランチをmainブランチに一本化し、全てのPull Requestは原則mainにマージする方針です。リリース前にreleaseブランチを作成し、そこでQAの実施とリリース作業を行ったあとにタグを打ちmainにマージします。
mainブランチに常にマージする形ですと開発途中の機能も一緒にリリースしてしまうことになりますが、Feature Flagを用いて開発中の機能はリリース時には閉じるようにしています。
struct FeatureView: View {
@Resolve(\.featureFlagClient) var featureFlag
var body: some View {
// 開発環境ではデバッグ画面からon/offを切り替えることが可能
if featureFlag.isAvailable(Flags.markSheet) {
NavigationLink { ... }
} else {
EmptyView()
}
}
}
github-flowを採用することでブランチがシンプルになり、マージし忘れやfeatureブランチの管理などから開放され、とても快適に開発ができています。
また、CI/CDに関してはGitHub Actions Self-Hosted runnerとBitriseの二刀流で行っています。それぞれのCIサービスは以下のような使い分けをしています。
- GitHub Actions self-hosted runner
- Unit Test
- IssueやPRのイベントをトリガーとしたビルド
- コード生成及びPRの作成
- Bitrise
- App Store配布
- Firebase App Distribution配布
Self-Hosted RunnerはもともとBitriseの課金プラン変更を見越して導入しましたが若干動作が不安定なところもあり、Bitriseのコストダウンも兼ねたCodemagicへの移行も検討中です。
プロジェクト構成
ライブラリ及び依存関係解決について、以前はCocoaPods+CarthageとEmbedded Frameworkという構成でしたが現在はいわゆる「Swift Package Manager中心の構成」にほとんど移行が完了しました。2,3個Carthageとxcframework管理されているものがありますが、こちらもいずれはSwiftPMのみで管理されるような形に持っていく予定です。
Package.swift
の構成は以下のような形になっており、基本的に機能ごとにモジュールが切られており、メインのmikanアプリが依存するAppFeature
、各プレビューアプリが依存するDesign SystemFeature
、BookReaderFeature
などのFeature Moduleなど依存が最小限になるように区切られています。
let package = Package(
name: "Mikan",
platforms: [
.iOS(.v14),
],
products: [
.library(name: "AppFeature", targets: ["AppFeature"]),
.library(name: "DesignSystemFeature", targets: ["UIComponents", "DesignToken"]),
.library(name: "BookReaderFeature", targets: ["BookReader"]),
...
],
dependencies: [
.package(url: "https://github.com/firebase/firebase-ios-sdk", exact: "10.1.0"),
...
],
targets: [
.target(name: "AppFeature", dependencies: [
"BookReader",
...
]),
.target(name: "UIComponents", dependencies: [
"DesignToken",
]),
.target(name: "BookReader", dependencies: [
"UIComponents",
.product(name: "ComposableArchitecture", package: "swift-composable-architecture")
]),
...
]
)
Embedded FrameworkからSwiftPMに移行することで格段にモジュールの分割と依存解決が表現しやすくなりました。モジュール分割の方針については基本的には各フィーチャーごと、そして機能ごとに分割をしています。
Module Stack
今のmikanアプリのモジュールスタックは以下のような形に変えていっています。
各Featureごとに最小限の依存にすることでプレビューアプリのビルドを高速で回せるようにしています。DIに関してはswift-composable-architectureのDependency AnnotationやSwiftLeeで紹介されていた方法などとと同じようなサービスロケータの仕組みを自前で実装して使っています。サービスロケータを管理するモジュールでサービスのインターフェースのみに依存させるように設計し、各サービスの実装やサービスが依存するFirebaseなどの重たいライブラリに依存してしまわないようにしています。
public protocol AppServicesContainer {
var analytics: ActionLogAnalytics { get }
}
public typealias Resolve<V> = DependencyResolve<V, AppServicesContainer>
@propertyWrapper
public struct DependencyResolve<V, S> {
...
}
public struct BookReader: ReducerProtocol {
@Resolve(\.analytics) var analytics
}
SwiftUI, SwiftUI Preview
SwiftUIへの移行もこの四半期でかなり力を入れて取り組んでいた所です。9月ごろにDeployment TargetをiOS14に更新しサポート対象OS全てでSwiftUIが使えるようになって以降、新しく画面を追加する場合は原則全てSwiftUIで実装するようにしています。既存の画面に関しても、大きく手を入れる所は積極的にSwiftUIに差し替えを進めています。
SwiftUIでは実装が難しい所はちょくちょく出てきますが、ワークアラウンドでUIViewRepresentableなどでUIKitのしさんが使えますし、UIKitで何百行も書かないと実現できなかったコンポーネントが数十行で書け、さらに宣言的でUIとしての表現力が高いコードがかけるSwiftUIは開発効率の劇的な改善が見込めると実感し、導入に踏み切りました。
SwiftUIと合わせて魅力的なSwiftUI Previewですが、PreviewアプリのTargetからは実装を限りなく除外、Mockのみに依存させておりFirestoreやその他大きいライブラリへの依存をPreviewから逃がせるようなモジュール依存にすることでSwiftUI Preview実行時のビルドを高速化しています。
まだまだSwiftUI Previewが不安定な場面はありますが、このような構成にしてからかなりSwiftUI Previewが安定して使えていると思います。
またSwiftUIにすることでUIコンポーネントもだいぶ実装しやすくなりました。
共通で使うコンポーネントに関してはUIComponentsモジュールに詰め込み、DesignSystemアプリでざっと確認できるようにしています。これをベースにデザイナーとデザインシステムについてコンポーネントレベルで詳細を詰めることができるようになったのでかなり便利です。
まだまだ移行したてなので少ないですが、今後SwiftUIで書かれる画面を増えるのと合わせてここも充実させていく予定です。
XcodeGenでPreview App生成を楽に
上記のようなコンポーネント確認用Appや各Featureの画面確認用のPreview Appは、今の所各Featureごとに作っています。各機能開発時に限りなく短時間で開発と実行・確認のサイクルを回すことができています。
以前Embedded Framework生成時に使っていたXcodeGenはPreview App生成用に現在も利用しています。XcodeGenにはproject.yml
を分割しincludeする事ができるため、Preview App共通で定義する項目をshared_preview.yml
として定義し、各Preview App側でincludeすることで最小限の実装でPreview Appを新たに作ることが可能であり、diffも最小限にすることが可能です。XcodeGenは都度xcodeprojの再生成が必要なので手間ですが、今ではほぼPreview Appの生成にしか使っておらず必要な時に生成すればよいので快適なPreview生活が送れています。
# shared_preview.yml
attributes:
ORGANIZATIONNAME: "mikan Co., Ltd."
schemes:
Preview:
..
packages:
Mikan:
..
options:
schemePathPrefix: "../"
..
settings:
base:
..
# feature_preview.yml
include: ../shared_preview.yml
targets:
PreviewApp:
type: application
platform: iOS
sources:
- path: Sources
dependencies:
- package: Mikan
product: SomeFeature
Future of mikan iOS
現状のmikan iOSの開発体制とプロジェクトの今についてピックアップしてまとめてみました。
このiOSの進化に合わせて、今後対応していく予定の項目についていくつかご紹介します。
まずはダークモードへの対応です。
今のmikanアプリでは現在Light Modeでしか提供できていません・・・。今やダークモードに対応していないアプリは珍しいぐらいになり、iOSアプリとして提供する以上対応して当たり前といえる機能と言えます。
現在は試験的に開発環境でダークモードを適用しており、細かい部分のレイアウト崩れに対応している所です。ユーザの要望も多いダークモード、来年早めにリリースできるよう尽力しています。
続いてSplit ViewなどiPadならではの機能です。
mikanはおかげさまで高校生など学生にも多く使っていただいているアプリです。学校でmikanを使う場合、スマートフォンよりもタブレットで使ってもらうことが多いです。
mikanはiPad向けにも提供されているアプリではありますが、今の所Split ViewなどのiPadならではの機能が使えなかったり、拡大されたスマートフォンのような画面になっていてiPadに最適化されたUIとは言えません。Split ViewやiPadにおいて使いやすい体験を設計し直していきたいと考えています。
その他にもiOS、iPadOSアプリとしての「あたりまえ品質」を高いレベルで提供できるよう来年以降も開発・改善を進めていきたいです。
まとめ
現状のmikanのiOSアプリについて開発的な視点からざっくりとまとめてみました。それぞれざっとまとめて細かい所は端折ってしまったため、気になることがあればぜひともmikanメンバーと一緒に話せると嬉しいです!
それでは、mikan Advent Calendar 2022を引き続きお楽しみください。
Discussion