swiftui-view-refactor について
SwiftUI のビューを小さく明示的な型へリファクタリングし、ビジネスロジックをビューの外に保つためのスキルです。状態管理、サブビュー、安定したビュー構造、Observation の使い方を案内します。
主な機能
- 01ビューのプロパティ順序
- 02MV優先の状態管理
- 03専用サブビューへの抽出
- 04bodyからアクションを分離
- 05安定したビュー階層の維持
- 06Observationラッパーの指針
活用シーン
- 01長いSwiftUI画面をサブビューに分割
- 02ビューのbodyからビジネスロジックを分離
- 03オプショナルなビューモデルを使うビューのリファクタリング
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ openai/ plugins --skill swiftui-view-refactorインストール後、Claude Code で/swiftui-view-refactorを入力して読み込まれたか確認します。
同梱ファイルagents/openai.yamlreferences/mv-patterns.md
詳細
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として保持します。 - オブザーバブルは明示的に下位へ渡し、UI が実際に必要とする場合を除いて、オプショナルな状態は避けます。
- デプロイメントターゲットに iOS 16 以前が含まれる場合は、所有側で
@StateObjectを使い、レガシーなオブザーバブルモデルを注入する際は@ObservedObjectを使います。
ワークフロー
- 並び替えルールに従って、ビューの順序を整えます。
bodyからインラインアクションと副作用を取り除き、ビジネスロジックをサービス/モデルに移します。ビューには薄いオーケストレーションのみを残します。- 専用のサブビュー型を抽出して長い
bodyを短くします。多数の計算プロパティsome Viewで画面を組み立て直すことは避けます。 - 安定したビュー構造を確保します。トップレベルで
ifによる分岐の切り替えは避け、条件を局所的なセクション/モディファイアに移します。 - ビューモデルが存在する場合、または明示的に必要な場合は、オプショナルなビューモデルを、
initで初期化する非オプショナルな@Stateビューモデルに置き換えます。 - Observation の使い方を確認します。iOS 17 以降ではルートの
@Observableモデルに@Stateを使用し、レガシーラッパーはデプロイメントターゲットで必要な場合にのみ使用します。 - 動作を維持します。依頼がない限り、レイアウトやビジネスロジックは変更しません。
注意事項
- 大きな条件分岐ブロックや大きな計算プロパティ
some Viewより、小さく明示的なビュー型を優先します。 - 計算ビューのビルダーは
bodyの下に置き、ビューではない計算プロパティはinitの上に置きます。 - 優れた SwiftUI のリファクタリングでは、ビューを上から下へ、レイアウトと命令型ロジックを混在させず、データフローとレイアウトとして読めるようにします。
- MV を優先する指針とその根拠については、
references/mv-patterns.mdを参照してください。 - 上記の参考資料に加えて、SwiftUI API、Observation の動作、またはプラットフォームの指針が変更されている可能性がある場合は、ウェブ検索で最新の Apple Developer ドキュメントを確認してください。
大きなビューの扱い
SwiftUI のビューのファイルが約 300 行を超える場合は、積極的に分割します。多数の計算プロパティに複雑さを隠すのではなく、意味のあるセクションを専用の View 型として抽出します。アクションやヘルパーには // MARK: - コメント付きの private エクステンションを使用しますが、巨大な画面を小さなビュー型に分割する代わりとしてエクステンションを扱ってはいけません。抽出したサブビューが再利用される場合、または独立した意味を持つ場合は、専用のファイルに移します。
コメント
Markdown 対応