guide-swiftui-view-refactor について
SwiftUI コードを扱うコーディングエージェント向けに、ビューを小さく明示的な型へ整理する手順を示します。ビュー外にビジネスロジックを置き、標準の SwiftUI 状態パターンを優先して、原則として ViewModel の追加を避けます。
主な機能
- 01ビューのプロパティ順序規則
- 02MV 優先の状態・依存関係ガイド
- 03大きなビューのサブビュー分割
- 04操作と副作用を body の外へ移動
- 05安定したビュー構造と Observation パターン
活用シーン
- 01長い SwiftUI 画面のリファクタリング
- 02インラインのボタン処理をメソッドやサービスへ移動
- 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/publiclet@State/その他の保存プロパティ- 計算プロパティの
var(ビュー以外) initbody- 計算ビューとその他のビュー用ヘルパー
- ヘルパー/非同期関数
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を使います。
ワークフロー
- 並べ替えのルールに従って、ビューの順序を整えます。
bodyからインラインアクションと副作用を取り除き、ビジネスロジックをサービスやモデルに移して、ビューには薄いオーケストレーションだけを残します。- 専用のサブビュー型を抽出して長い
bodyを短くします。多数の計算プロパティsome Viewヘルパーで画面を再構築するのは避けます。 - ビューの構造を安定させます。トップレベルの
ifによる分岐の切り替えを避け、条件は局所的なセクションやモディファイアに移します。 - ビューモデルが存在する場合、または明示的に必要な場合は、オプショナルなビューモデルを、
initで初期化する非オプショナルな@Stateビューモデルに置き換えます。 - Observation の使い方を確認します。iOS 17 以降ではルートの
@Observableモデルに@Stateを使い、レガシーなラッパーはデプロイメントターゲットで必要な場合に限って使います。 - 動作を維持します。依頼がない限り、レイアウトやビジネスロジックを変更しないでください。
注意事項
- 大きな条件分岐や大きな計算プロパティ
some Viewより、小さく明示的なビュー型を優先します。 - 計算ビューのビルダーは
bodyの下に、ビュー以外の計算プロパティはinitの上に配置します。 - 適切にリファクタリングされた SwiftUI のビューでは、レイアウトと命令型ロジックが混在せず、データフローとレイアウトを上から下へ読み進められます。
- MV ファーストの指針と根拠については、
references/mv-patterns.mdを参照してください。
大規模なビューの扱い
SwiftUI のビューのファイルが約 300 行を超える場合は、積極的に分割してください。多数の計算プロパティに複雑さを隠すのではなく、意味のあるセクションを専用の View 型に抽出します。アクションやヘルパーには // MARK: - コメントを付けた private 拡張を使用できますが、巨大な画面を小さなビュー型に分割する代わりとして拡張を使わないでください。抽出したサブビューを再利用する場合、またはそれ自体に独立した意味がある場合は、独自のファイルに移してください。