Skip to content

Refactor: SPMマルチモジュール構成への移行 #75

@stotic-dev

Description

@stotic-dev

概要

現在 homete/ 配下にフラットに配置されているコードを、Swift Package Manager(SPM)のローカルパッケージとして Feature ベースの複数モジュール に分割する。ビルド時間の短縮・モジュール境界の明確化・Previewの軽量化・テスタビリティの向上を目的とする。

背景・目的

現在のプロジェクト構造(Swift ファイル 131 本)では、すべてのコードがメインターゲット homete 内に存在しており、以下の課題が生じている。

  • 変更箇所が少ない場合でもターゲット全体が再コンパイルされ、ビルド時間が長い
  • レイヤー間の依存方向がコードレベルで強制されず、誤った依存が生まれやすい
  • テストターゲットがメインターゲット全体に依存するため、ユニットテストの実行が遅い
  • Feature単体でのPreviewビルドができず、プレビューが重い
  • 将来的にチームが拡大した場合、同一ターゲット内の変更が衝突しやすい

アーキテクチャ方針

モジュール構成

HometeDomain/
  ├── Domain Models (Account, HouseworkItem, CohabitantData...)
  ├── Client Protocols + previewValue
  ├── Stores (AccountStore, HouseworkListStore, CohabitantStore, AccountAuthStore)
  └── AppRoute + RouteResolver

HometeUI/
  ├── DesignSystem (色、フォント、共通スタイル)
  ├── 共通コンポーネント (ボタン、カード等)
  └── ViewUtilities (Alert、Navigation等)
  → HometeDomain に依存

AuthFeature/         → HometeDomain + HometeUI に依存
HouseworkFeature/    → HometeDomain + HometeUI に依存
CohabitantFeature/   → HometeDomain + HometeUI に依存
SettingFeature/      → HometeDomain + HometeUI に依存
HomeFeature/         → HometeDomain + HometeUI に依存

homete (メインターゲット)/
  ├── Client liveValue 実装
  ├── Services (FirestoreService, SignInWithAppleService...)
  ├── RouteResolver 実態
  ├── RootView / AppTabView / DependenciesInjectLayer
  └── Store の初期化・Environment注入

依存方向

homete (メインターゲット)  → Feature modules → HometeUI → HometeDomain
  • 全 Feature モジュールは HometeDomain + HometeUI のみに依存(Feature 間の直接依存は禁止)
  • Feature モジュールは純粋に View 層のみを持つ
  • HometeUI は HometeDomain に依存(ドメインモデルを表示に使う場合)

Feature 間の画面遷移(RouteResolver パターン)

Feature 間で直接依存せずに画面遷移を実現するため、HometeDomain に AppRoute enum と RouteResolver を定義し、メインターゲットで実態を DI する。

// HometeDomain 側
enum AppRoute: Hashable {
    case houseworkDetail(HouseworkItem)
    case houseworkApproval(HouseworkItem)
    case registerHousework(CohabitantData)
    case cohabitantRegistration
    case setting
}

struct RouteResolver {
    var resolve: @MainActor (AppRoute) -> AnyView
}

extension EnvironmentValues {
    @Entry var routeResolver: RouteResolver = .preview
}

// Preview 用
extension RouteResolver {
    static let preview = RouteResolver { route in
        AnyView(Text("Preview: \(String(describing: route))"))
    }
}
// Feature 側(例: HomeFeature)
struct HomeView: View {
    @Environment(\.routeResolver) private var router

    var body: some View {
        .sheet(isPresented: $isShowSetting) {
            router.resolve(.setting)
        }
    }
}
// メインターゲット側
let resolver = RouteResolver { route in
    switch route {
    case .houseworkDetail(let item):
        AnyView(HouseworkDetailView(item: item))
    case .setting:
        AnyView(SettingView())
    // ...
    }
}

RootView()
    .environment(\.routeResolver, resolver)

Store の配置方針

Store は HometeDomain に配置する。理由:

  • Store は特定の画面に依存しない、全画面で再利用可能なビジネスルール・ドメインモデルを提供するオブジェクト
  • 複数 Feature から共有される(例: HouseworkListStore は HouseworkBoardView, HomeView 等で利用)
  • Client Protocol に依存するが、これも HometeDomain 内にあるため整合性が取れる
  • メインターゲットで Store を初期化し、Environment 経由で各 Feature に注入

実装タスク

  • Phase 1: HometeDomain パッケージの切り出し(Domain Models + Client Protocols + Stores + AppRoute)
  • Phase 3: Feature パッケージの切り出し(AuthFeature, HouseworkFeature, CohabitantFeature, SettingFeature, HomeFeature)
  • Phase 4: メインターゲットの整理(Services + liveValue実装 + RouteResolver実態 + RootView)
  • Phase 5: テストターゲットの整理(モジュールごとのテストターゲット追加 or 既存の hometeTests/ を更新)
  • Phase 6: CI ビルド時間・テスト実行時間の計測・比較

補足

  • 段階的移行(Phase 1 から順に PR を分けてマージ)を推奨。1 PR での全移行はリスクが高い。
  • ProjectTools(SwiftLint / Danger)は現行のローカルパッケージ形式のまま変更不要。
  • Firebase iOS SDK 等のサードパーティ依存はメインターゲット(Services 層)が保持する。
  • Swift 6 strict concurrency との整合性(actor 分離、Sendable 等)は各 Phase で確認する。
  • Feature 間の循環依存を防ぐため、Feature 間の画面遷移は必ず RouteResolver パターンを使用する。

Sub-issues

Metadata

Metadata

Assignees

Labels

No labels
No labels

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions