mcpField
S
swiftui-view-refactor

swiftui-view-refactor

github.com/dimillian開発4.0k2.3k 回インストール30 週間前に更新
リポジトリ

swiftui-view-refactor について

swiftui-view-refactorは、既存の動作を保ちながらSwiftUIビューを整理するスキルです。小さく明示的なサブビューと軽量な状態表現を重視し、ビジネスロジックをビュー本体から分離します。

主な機能

  1. 01ビューのプロパティ順序の規約
  2. 02SwiftUI標準の状態管理を優先
  3. 03専用サブビューへの分割
  4. 04bodyから副作用を分離
  5. 05安定したビュー構造の指針

活用シーン

  1. 01複数のセクションを持つ大きなビューの分割
  2. 02ビュー内のビジネスロジックや操作の分離
  3. 03状態管理と依存性注入の見直し

インストール

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

$npx skills add https://github.com/dimillian/skills --skill swiftui-view-refactor

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

同梱ファイルagents/openai.yamlreferences/mv-patterns.md

詳細

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 として保持します。
  • オブザーバブルは明示的に下位へ渡し、UI が実際に必要とする場合を除いて、オプショナルな状態は避けます。
  • デプロイメントターゲットに iOS 16 以前が含まれる場合は、所有側で @StateObject を使い、レガシーなオブザーバブルモデルを注入する際は @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 拡張を使用できますが、巨大な画面を小さなビュー型に分割する代わりとして拡張を使わないでください。抽出したサブビューを再利用する場合、またはそれ自体に独立した意味がある場合は、独自のファイルに移してください。

コメント