swift-mvvm について
Swiftプロジェクトに取り組むコーディングエージェント向けのMVVMスキルで、ViewModelの作成とリファクタリングを扱います。UIフレームワークからの分離、テスト可能な部品、プロトコルによる依存性注入を重視します。
主な機能
- 01ViewModelからUIフレームワークのインポートを排除
- 02状態、インテント、依存関係の調整を分離
- 03純粋なロジックと副作用を抽出
- 04プロトコル経由で依存性を注入
- 05メインアクターの状態とタスクのキャンセルを管理
- 06ViewModelのテスト方法を案内
活用シーン
- 01SwiftUI、UIKit、AppKitプロジェクトへの機能追加
- 02肥大化したViewModelのリファクタリング
- 03状態管理やスレッドの問題の調査
- 04依存関係のモック化と状態テストの改善
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ tobitech/ swift-mvvm --skill swift-mvvmインストール後、Claude Code で/swift-mvvmを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
README.mdreferences/adding-new-features.mdfile-naming-conventions.mdmodule-structure.mdcommon-patterns.mdmvvm-overview.mdintegration-patterns.mdmvvm-style-guide.mdcontroller-vs-coordinator.mdanti-patterns.mdservices-vs-feature-services.mdwhere-things-go.mdstate-management.mdtesting-considerations.mdtemplates/AppKitRevealer.swiftAppKitViewController.swiftObservationViewModel.swiftServiceUseCaseControllerAndPureLogic.swiftCombineViewModel.swiftProtocolAdapters.swiftViewModelTests.swiftSwiftUICombineView.swiftSwiftUIObservationView.swift詳細
Swift MVVM スキル
Swift プロジェクトでより優れた MVVM コードを作成できるよう、エージェントを支援します。
基本方針
- ViewModel を UI フレームワークに依存させないでください。 ViewModel では
SwiftUI、UIKit、AppKitをインポートしないでください。 - ViewModel は主に (1)状態、(2)インテントメソッド、(3)依存関係の調整で構成します。
- 作業を小さくテスト可能な単位(純粋な構造体/関数、ユースケース、コントローラー、リポジトリ、マッパー、フォーマッター)に分割します。
- プロトコルを使って依存性注入を行い、すべてをモック可能にします。
- 整理のためにエクステンションを積極的に使います(特にプロトコル適合)。
使用する場面
ユーザーから次の依頼があった場合に、このスキルを使用します。
- アーキテクチャを適切に保ちながら、SwiftUI/UIKit/AppKit の機能を追加またはリファクタリングする。
- ViewModel(状態、インテント、エフェクト)を作成または改善し、ロジックを View/ViewController から移動する。
- 状態管理の問題(誤ったラッパー、スレッドに関する警告、不安定なバインディング)を修正する。
- テスト容易性を高める(プロトコルベースの依存関係、モック/フェイク、決定論的な状態更新)。
- 「肥大化した ViewModel」を関心ごとごとに分割する。
最初に確認すること(コードを書く前)
- UI 技術を特定します。SwiftUI、UIKit、AppKit のいずれかです。
- デプロイメントターゲットを特定します。
- iOS 17+/macOS 14+ を利用できる場合は、Observation(
@Observable)を優先します。 - それ以外の場合は、
ObservableObjectと@Publishedを使った Combine ベースの方法を優先します。
- iOS 17+/macOS 14+ を利用できる場合は、Observation(
- 既存のプロジェクトスタイル(命名、フォルダー、DI の方法、ネットワークレイヤー)に合わせます。
MVVM の責務
View 層
SwiftUI の View/UIKit の UIViewController/AppKit の NSViewController:
- レイアウトを宣言し、状態にバインドします。
- ユーザーインテント(タップ、選択、テキストの変更)を ViewModel に送信します。
- UI のみに関わる処理(ナビゲーション、アラートの表示、AppKit のパネル、ファーストレスポンダーなど)を所有します。
ViewModel
- 画面の状態(読み込み中/データ/エラー、派生 UI 値)を所有します。
- 注入された依存関係を通じてエフェクトを調整します。
- View から任意の内部処理を呼び出すのではなく、
onAppear()、refresh()、didTap…などのインテントメソッドを公開します。
ドメイン/サービス
- データの取得、キャッシュ、永続化、デコード、検証をカプセル化します。
- UI フレームワークから独立させる必要があります。
厳守事項:ViewModel で UI フレームワークをインポートしない
ViewModel では次のものをインポートしないでください。
SwiftUIUIKitAppKit
ViewModel でプラットフォーム固有の動作が必要な場合は、UI に依存しないモジュール(Foundation のみ)に小さなプロトコルを定義し、View 層(またはプラットフォームアダプターモジュール)でプラットフォーム固有の実装を提供します。
例:
// In a Foundation-only target / file.
protocol FileRevealing {
func reveal(_ url: URL)
}
// In AppKit layer.
import AppKit
struct WorkspaceFileRevealer: FileRevealing {
func reveal(_ url: URL) {
NSWorkspace.shared.activateFileViewerSelecting([url])
}
}
Any “AppKit-y” or “UIKit-y” behavior inside the ViewModel is a **smell**.
## Preferred ViewModel shapes
Pick one based on scope.
### Pattern A: Simple screen
- `State` struct (nested)
- intent methods
- async `load()` with cancellation
### Pattern B: Complex screen
- `State` struct (nested)
- `Action` enum + `send(_:)`
- reducer-like switch for state transitions
- side effects delegated to injected units
## Keep state structured (avoid a ViewModel with 30 vars)
Prefer:
```swift
struct State: Equatable {
var view = ViewState()
var content = ContentState()
var alerts = AlertsState()
struct ViewState: Equatable {
var isLoading = false
var title = ""
}
struct ContentState: Equatable {
var rows: [Row] = []
var emptyMessage: String? = nil
}
struct AlertsState: Equatable {
var error: ErrorState? = nil
}
}
肥大化した ViewModel のリファクタリング(優先順位)
大きな VM を縮小する場合は、次の順にリファクタリングします。
- まず純粋なロジックを切り出す
- IO を伴わない計算を純粋な構造体または純粋な関数に移します。
- 例:フィルタリング、ソート、ドメインモデルから行モデルへのマッピング、フォーマット、検証、状態リデューサー。
- 副作用をコントローラーに切り出す(引き続きテスト可能にする)
- IO とオーケストレーションを行う、小さな「エフェクト」単位を作成します。
- それらをプロトコルの背後に置き、VM に注入します。
- 例:
LoadExamplesUseCase、ExamplesController、AnalyticsTracking、FileRevealing。
- VM は状態とインテントのみを担うようにする
- VM はインテントを純粋ロジック/エフェクトの単位に転送し、状態を設定します。
並行処理とキャンセルのルール
- UI の状態変更はメインアクター上で行います。
- 実行中のタスクを保持し、新しいリクエストの開始時や View が表示されなくなるときにキャンセルします。
SwiftUI 連携のルール
Observation(@Observable)
- ViewModel インスタンスは
@State(所有者)で保持し、下位の View に渡します。
Combine(ObservableObject)
- View が ViewModel を作成/所有する場合は
@StateObjectを使います。 - View に ViewModel が渡される場合は
@ObservedObjectを使います。
依存性注入のルール
- ViewModel はイニシャライザーで依存関係を受け取ります。
- 依存関係にはプロトコルを使用し、本番用の実装とテスト用のモック/フェイクを用意します。
- 小さな単位の注入を優先します。
- 純粋なロジック:
RowBuilder、Validator、Reducer - 副作用:
UseCase、Controller、Repository - プラットフォームアダプター:
FileRevealing、URLOpeningなど
- 純粋なロジック:
巨大な ViewModel を避ける
問題の兆候:
- UI フレームワークをインポートしている。
- URLSession や JSON デコードを直接実行している。
- NSAlert/UIAlertController を構築している。
- NSWorkspace/UIApplication に依存している。
- 複雑なロジックを使って、あらゆる内容をインラインでフォーマットしている。
- ナビゲーション、分析、ネットワーキング、キャッシュをすべてまとめて処理している。
リファクタリングの手順:
- 純粋な
RowBuilderまたはReducerを切り出します。 - ビジネスルールのための
UseCaseを切り出します。 - IO のための
Repositoryを切り出します。 - 複数のサービスを調整する、副作用を扱う
Controllerを切り出します。 - UI 操作用のプラットフォームアダプター(小さなプロトコル)を切り出します。
出力に関する期待事項
作成やリファクタリングを行う際は:
- ファイルごとにまとめた、差分を確認しやすい単位でコードを提示します。
- 小さく、組み合わせ可能な関数を優先します。
- 整理にはエクステンションを使用します(例:プロトコルへの準拠、ヘルパーのグループ化)。
FooViewModel、FooState、FooUseCase、FooRepository、FooController、FooRowBuilderのように、明確な名前を付けます。- ViewModel の状態遷移と、切り出した純粋なロジックのテストを追加します。
テストのガイダンス
新しいテストには Swift Testing(import Testing、@Test、#expect、#require)を優先します。
- 純粋なロジックの単位を直接テストします(高速で決定的です)。
- モックを注入して、ViewModel の状態遷移(成功/失敗/キャンセル)をテストします。
テンプレート
以下からコピーして使用します。
templates/ObservationViewModel.swifttemplates/CombineViewModel.swifttemplates/AppKitViewController.swifttemplates/ProtocolAdapters.swifttemplates/ServiceUseCaseControllerAndPureLogic.swifttemplates/ViewModelTests.swift(Swift Testing)
追加のリソース(必要な場合のみ読み込み)
- MVVM の概要:references/mvvm-overview.md
- モジュール構成:references/module-structure.md
- ファイル命名規則:references/file-naming-conventions.md
- 各要素の配置場所:references/where-things-go.md
- 一般的なパターン:references/common-patterns.md
- 新機能の追加:references/adding-new-features.md
- 統合パターン:references/integration-patterns.md
- アンチパターン:references/anti-patterns.md
- テストに関する考慮事項:references/testing-considerations.md
- サービスと機能サービスの違い:references/services-vs-feature-services.md
- Controller と Coordinator の違い:references/controller-vs-coordinator.md
- 状態管理:references/state-management.md
コメント
Markdown 対応