Loki(ロキ)は、サ活の記録に特化したアプリです。
誰でもこのプロジェクトを開発できます。
- macOS 13.5+
- Xcode 15.0 (Swift 5.9)
- Make
- UIの実装: SwiftUI
- アーキテクチャ: MVVM
- ブランチモデル: GitHub flow
-
このプロジェクトをクローンします。
$ git clone https://github.com/uhooi/Loki.git $ cd Loki
-
Swiftプロジェクトの高速ビルドを有効にします。(任意)
$ defaults write com.apple.dt.XCBuild EnableSwiftBuildSystemIntegration 1
-
make setup
を実行します。
セットアップが完了すると、自動的にXcodeでワークスペースが開きます。
モジュール分割
- できる限りSwiftパッケージにソースコードを寄せる
- プロジェクトには最低限のファイルのみ含める
Apps
・Features
・Data
・Core
の4層に分ける
- アプリのエントリポイントで、ルートナビゲーションロジックを格納する
- 基本的にすべてのFeatureモジュールに依存する
- Dataモジュールに依存してはいけない
- Coreモジュールに依存していい
参考: https://developer.android.com/topic/modularization/patterns#app-modules
- 各機能のビューやビューモデルを格納する
- Appモジュールに依存してはいけない
- ほかのFeatureモジュールに依存してはいけない
- DataやCoreモジュールに依存していい
参考: https://developer.android.com/topic/modularization/patterns#feature-modules
- リポジトリやモデルを格納する
- AppやFeatureモジュールに依存してはいけない
- できる限りほかのDataモジュールに依存しない
- Coreモジュールに依存していい
参考: https://developer.android.com/topic/modularization/patterns#data-modules
- 複数のモジュールが共通で使う処理を格納する
- AppやFeature、Dataモジュールに依存してはいけない
- ほかのCoreモジュールに依存していい
参考: https://developer.android.com/topic/modularization/patterns#common-modules
コーディングルール
- できる限りAPI Design Guidelinesに従う
- できる限り
any
よりsome
を使う
- ビューは単体テストを書かない
- UIは手動でテストすることが多く、費用対効果に合わないため
- できる限り分岐(
if
・switch
)を入れない- 単体テストを書かないため
- できる限り
Task { ... }
をビューに書く- ビューモデルの単体テストが書きづらくなるため
- 状態はビューモデルの
uiState
に集約し、ビューでは保持しない- つまり
@State
を使わず、@StateObject
はビューモデルのみに付ける @Published
もビューモデルのuiState
のみに付ける
- つまり
- できる限り
@AppStorage
を使わず、UserDefaultsへはData
層でアクセスする- ビュー層のプロパティを永続化したい場合のみ使う
View
以外では使わない- 参考: https://twitter.com/noppefoxwolf/status/1612800897654075392
- 画面全体のビュー(ここでは「親ビュー」と呼ぶ)を
{画面名}Screen
と命名する - 以下の処理を親ビューに書く
- ビューモデルの保持
- ナビゲーションロジック
NavigationStack { ... }
やNavigationSplitView { ... }
、.navigationTitle()
、.navigationBarTitleDisplayMode()
など- 例: https://github.com/uhooi/Loki/blob/8d22650afeb777bd15e858bfad2b6ece06dcb152/TotonoiPackage/Sources/Features/Sakatsu/SakatsuList/SakatsuListScreen.swift#L8
https://github.com/uhooi/Loki/blob/8d22650afeb777bd15e858bfad2b6ece06dcb152/TotonoiPackage/Sources/Features/Sakatsu/SakatsuList/SakatsuListScreen.swift#L19
https://github.com/uhooi/Loki/blob/8d22650afeb777bd15e858bfad2b6ece06dcb152/TotonoiPackage/Sources/Features/Sakatsu/SakatsuInput/SakatsuInputScreen.swift#L45
- ツールバー、シートやアラートなど、画面全体に関わる表示
.toolbar { ... }
、.sheet()
や.alert()
など- 例: https://github.com/uhooi/Loki/blob/8d22650afeb777bd15e858bfad2b6ece06dcb152/TotonoiPackage/Sources/Features/Sakatsu/SakatsuList/SakatsuListScreen.swift#L20-L36
https://github.com/uhooi/Loki/blob/8d22650afeb777bd15e858bfad2b6ece06dcb152/TotonoiPackage/Sources/Features/Sakatsu/SakatsuList/SakatsuListScreen.swift#L45-L103
- 親ビューは最低限の処理のみ書き、ほかは直下の子ビューに書く
- ビューモデルを直接参照せず、状態ホイスティングを適用する
- 参考: https://developer.android.com/jetpack/compose/state#state-hoisting
- つまり表示する現在の値と、値を変更するイベントのハンドラを親ビューから渡す
@Binding
や${変数名}
は使わない
- 1画面1ビューモデルとする
{画面名}ViewModel
と命名するUIKit
やSwiftUI
などのUIフレームワークをインポートしない- ビューモデルにUIを持ち込みたくないため
@MainActor
を付けたfinal class
とし、ObservableObject
に準拠する- 状態を
uiState
で一元管理し、private(set)
にしてビューから状態を変更させない- 構造体名は
{画面名}UiState
とする - 例: https://github.com/uhooi/Loki/blob/8d22650afeb777bd15e858bfad2b6ece06dcb152/TotonoiPackage/Sources/Features/Sakatsu/SakatsuList/SakatsuListViewModel.swift#L5-L13
https://github.com/uhooi/Loki/blob/8d22650afeb777bd15e858bfad2b6ece06dcb152/TotonoiPackage/Sources/Features/Sakatsu/SakatsuList/SakatsuListViewModel.swift#L35
- 構造体名は
- エラーは画面ごとに1つの列挙型へまとめ、
uiState
で1つのみ保持する- エラーはアラートで表示することが多く、1つの型になっていると複数同時に表示されないことが保証されるため
- エラー名は
{画面名}Error
とする - 例: https://github.com/uhooi/Loki/blob/8d22650afeb777bd15e858bfad2b6ece06dcb152/TotonoiPackage/Sources/Features/Sakatsu/SakatsuList/SakatsuListViewModel.swift#L15-L29
https://github.com/uhooi/Loki/blob/8d22650afeb777bd15e858bfad2b6ece06dcb152/TotonoiPackage/Sources/Features/Sakatsu/SakatsuList/SakatsuListViewModel.swift#L12
- ビューのイベントをハンドリングする
パッケージ管理
Package.swift
のみで管理する
- できる限り使わない
- どうしても使う場合、適切に管理する
- Build Tool PluginまたはCommand Pluginで管理する
- 用意されていない場合は自作してOSSにPRを送る
- マージされない場合、本リポジトリまたはプラグイン用のリポジトリを作成してコミットする
- どうしてもPluginを用意できない場合、Mintで管理する
- できる限り使わない
- どうしても使う場合、Bundlerで管理する
- できる限り使わない
- どうしても使う場合、適切に管理する
貢献をお待ちしています