mcpField
G
guide-swiftui-view-refactor

guide-swiftui-view-refactor

github.com/prisma-labs-dev開発349193 回インストール5 週間前に更新
リポジトリ

guide-swiftui-view-refactor について

SwiftUI コードを扱うコーディングエージェント向けに、ビューを小さく明示的な型へ整理する手順を示します。ビュー外にビジネスロジックを置き、標準の SwiftUI 状態パターンを優先して、原則として ViewModel の追加を避けます。

主な機能

  1. 01ビューのプロパティ順序規則
  2. 02MV 優先の状態・依存関係ガイド
  3. 03大きなビューのサブビュー分割
  4. 04操作と副作用を body の外へ移動
  5. 05安定したビュー構造と Observation パターン

活用シーン

  1. 01長い SwiftUI 画面のリファクタリング
  2. 02インラインのボタン処理をメソッドやサービスへ移動
  3. 03条件分岐するルートビューを安定したレイアウトへ変更

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/prisma-labs-dev/apple-skills --skill guide-swiftui-view-refactor

インストール後、Claude Code で/guide-swiftui-view-refactorを入力して読み込まれたか確認します。

同梱ファイルreferences/mv-patterns.md

詳細

ガイドスキル — これはエキスパート向けのワークフロー/パターンガイドであり、API リファレンスドキュメントではありません。 これは、Thomas Ricouard による Dimillian/Skills が元になっています。MIT ライセンス。

SwiftUI ビューのリファクタリング

概要

SwiftUI のビューを、小さく、明示的で、安定したビュー型へリファクタリングします。基本はバニラの SwiftUI とします。ビュー内にローカル状態を置き、共有依存関係は環境から取得し、ビジネスロジックはサービス/モデルに置きます。リクエストまたは既存コードで明らかに必要な場合に限り、ビューモデルを使用します。

基本ガイドライン

1) ビューの順序(上から下へ)
  • 既存ファイルに、維持すべきより強い局所的な規約がない限り、次の順序を徹底します。
  • Environment
  • private/public let
  • @State/その他の保存プロパティ
  • 計算プロパティの var(ビュー以外)
  • init
  • body
  • 計算ビューとその他のビュー用ヘルパー
  • ヘルパー/非同期関数
2) MV を基本とし、MVVM にしない
  • ビューは軽量な状態表現とオーケストレーションの場とし、ビジネスロジックを格納する場所にはしません。
  • ビューモデルを使う前に、@State、@Environment、@Query、.task、.task(id:)、onChange を優先します。
  • サービスと共有モデルは @Environment 経由で注入し、ドメインロジックはビュー本体ではなくサービス/モデルに置きます。
  • ローカルなビュー状態をそのまま複製したり、環境の依存関係をラップしたりするだけのビューモデルは導入しません。
  • 画面が大きくなってきたら、新しいビューモデル層を作る前に UI をサブビューへ分割します。
3) 計算プロパティの some View ヘルパーより、専用のサブビュー型を強く優先する
  • 1画面分を超えるほど長い、または複数の論理セクションを含む body プロパティには注意します。
  • 状態、非同期処理、条件分岐を含む場合や、独立したプレビューに値する場合は特に、複雑なセクションを専用の View 型として切り出すことを優先します。
  • 計算プロパティの some View ヘルパーは少数かつ小さく保ちます。private var header: some View のような断片だけで画面全体を構成しないでください。
  • サブビューへ切り出す際は、親の状態全体を渡すのではなく、小さく明示的な入力(データ、バインディング、コールバック)を渡します。
  • 切り出したサブビューが再利用可能になる、または独立した意味を持つようになったら、専用のファイルに移します。

推奨例:

var body: some View {
    List {
        HeaderSection(title: title, subtitle: subtitle)
        FilterSection(
            filterOptions: filterOptions,
            selectedFilter: $selectedFilter
        )
        ResultsSection(items: filteredItems)
        FooterSection()
    }
}

private struct HeaderSection: View {
    let title: String
    let subtitle: String

    var body: some View {
        VStack(alignment: .leading, spacing: 6) {
            Text(title).font(.title2)
            Text(subtitle).font(.subheadline)
        }
    }
}

private struct FilterSection: View {
    let filterOptions: [FilterOption]
    @Binding var selectedFilter: FilterOption

    var body: some View {
        ScrollView(.horizontal, showsIndicators: false) {
            HStack {
                ForEach(filterOptions, id: \.self) { option in
                    FilterChip(option: option, isSelected: option == selectedFilter)
                        .onTapGesture { selectedFilter = option }
                }
            }
        }
    }
}

避ける例:

var body: some View {
    List {
        header
        filters
        results
        footer
    }
}

private var header: some View {
    VStack(alignment: .leading, spacing: 6) {
        Text(title).font(.title2)
        Text(subtitle).font(.subheadline)
    }
}
3b) アクションと副作用を body の外へ切り出す
  • 複雑なボタンアクションをビュー本体にインラインで記述しないでください。
  • .task、.onAppear、.onChange、.refreshable の内部にビジネスロジックを隠さないでください。
  • ビューから小さなプライベートメソッドを呼び出し、実際のビジネスロジックはサービス/モデルに移すことを優先します。
  • body はビューコントローラーのようではなく、UI として読めるべきです。
Button("Save", action: save)
    .disabled(isSaving)

.task(id: searchText) {
    await reload(for: searchText)
}

private func save() {
    Task { await saveAsync() }
}

private func reload(for searchText: String) async {
    guard !searchText.isEmpty else {
        results = []
        return
    }
    await searchService.search(searchText)
}
4) 安定したビュー階層を保つ(トップレベルでの条件付きビュー切り替えを避ける)
  • if/else によってまったく異なるルート分岐を返す body や計算ビューは避けます。
  • 単一の安定したベースビューを使い、セクションやモディファイア(overlay、opacity、disabled、toolbar など)の内部に条件を置くことを優先します。
  • ルートレベルで分岐を切り替えると、アイデンティティの変動、広範囲の無効化、再計算の増加につながります。

推奨例:

var body: some View {
    List {
        documentsListContent
    }
    .toolbar {
        if canEdit {
            editToolbar
        }
    }
}

避ける例:

var documentsListView: some View {
    if canEdit {
        editableDocumentsList
    } else {
        readOnlyDocumentsList
    }
}
5) ビューモデルの扱い(すでに存在する場合、または明示的に要求された場合のみ)
  • ビューモデルは、標準ではなく、レガシーパターンまたは明示的な必要性に基づくパターンとして扱います。
  • リクエストまたは既存コードで明らかに必要とされない限り、ビューモデルを導入しないでください。
  • ビューモデルがある場合は、可能な限り非オプショナルにします。
  • 依存関係を init 経由でビューに渡し、ビューの init 内でビューモデルを作成します。
  • bootstrapIfNeeded パターンや、その他の遅延セットアップによる回避策は避けます。

例(Observation ベース):

@State private var viewModel: SomeViewModel

init(dependency: Dependency) {
    _viewModel = State(initialValue: SomeViewModel(dependency: dependency))
}
6) Observation の使用
  • iOS 17 以降では、@Observable な参照型を、それを所有するビュー内で @State として保存します。
  • Observable は明示的に下位へ渡し、UI で本当に必要な場合を除き、オプショナル状態は避けます。
  • デプロイメントターゲットに iOS 16 以前が含まれる場合は、所有側で @StateObject を使い、レガシーな Observable モデルを注入する際は @ObservedObject を使います。

ワークフロー

  1. 並べ替えのルールに従って、ビューの順序を整えます。
  2. body からインラインアクションと副作用を取り除き、ビジネスロジックをサービスやモデルに移して、ビューには薄いオーケストレーションだけを残します。
  3. 専用のサブビュー型を抽出して長い body を短くします。多数の計算プロパティ some View ヘルパーで画面を再構築するのは避けます。
  4. ビューの構造を安定させます。トップレベルの if による分岐の切り替えを避け、条件は局所的なセクションやモディファイアに移します。
  5. ビューモデルが存在する場合、または明示的に必要な場合は、オプショナルなビューモデルを、init で初期化する非オプショナルな @State ビューモデルに置き換えます。
  6. Observation の使い方を確認します。iOS 17 以降ではルートの @Observable モデルに @State を使い、レガシーなラッパーはデプロイメントターゲットで必要な場合に限って使います。
  7. 動作を維持します。依頼がない限り、レイアウトやビジネスロジックを変更しないでください。

注意事項

  • 大きな条件分岐や大きな計算プロパティ some View より、小さく明示的なビュー型を優先します。
  • 計算ビューのビルダーは body の下に、ビュー以外の計算プロパティは init の上に配置します。
  • 適切にリファクタリングされた SwiftUI のビューでは、レイアウトと命令型ロジックが混在せず、データフローとレイアウトを上から下へ読み進められます。
  • MV ファーストの指針と根拠については、references/mv-patterns.md を参照してください。

大規模なビューの扱い

SwiftUI のビューのファイルが約 300 行を超える場合は、積極的に分割してください。多数の計算プロパティに複雑さを隠すのではなく、意味のあるセクションを専用の View 型に抽出します。アクションやヘルパーには // MARK: - コメントを付けた private 拡張を使用できますが、巨大な画面を小さなビュー型に分割する代わりとして拡張を使わないでください。抽出したサブビューを再利用する場合、またはそれ自体に独立した意味がある場合は、独自のファイルに移してください。

コメント