swiftui-view-refactor について
swiftui-view-refactorは、既存の動作を保ちながらSwiftUIビューを整理するスキルです。小さく明示的なサブビューと軽量な状態表現を重視し、ビジネスロジックをビュー本体から分離します。
主な機能
- 01ビューのプロパティ順序の規約
- 02SwiftUI標準の状態管理を優先
- 03専用サブビューへの分割
- 04bodyから副作用を分離
- 05安定したビュー構造の指針
活用シーン
- 01複数のセクションを持つ大きなビューの分割
- 02ビュー内のビジネスロジックや操作の分離
- 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/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 のビューのファイルが約 300 行を超える場合は、積極的に分割してください。多数の計算プロパティに複雑さを隠すのではなく、意味のあるセクションを専用の View 型に抽出します。アクションやヘルパーには // MARK: - コメントを付けた private 拡張を使用できますが、巨大な画面を小さなビュー型に分割する代わりとして拡張を使わないでください。抽出したサブビューを再利用する場合、またはそれ自体に独立した意味がある場合は、独自のファイルに移してください。
コメント
Markdown 対応