ui-patterns について
SwiftUI アプリを開発・拡張するための UI パターンスキルです。ビュー構成、状態の所有箇所、ナビゲーションを案内し、SwiftUI 標準のデータフローと、操作を自身で管理するシートを重視します。
主な機能
- 01ビュー状態と所有箇所の設計
- 02小さく焦点を絞ったビューの構成
- 03SwiftUI の状態管理と環境注入
- 04非同期読み込みとエラー状態の処理
- 05既存プロジェクトの規約への準拠
活用シーン
- 01機能向けのリスト、詳細、編集画面の構築
- 02新規プロジェクトのタブとナビゲーションの設定
- 03シートと状態管理パターンの統一
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ patrickserrano/ skills --skill ui-patternsインストール後、Claude Code で/ui-patternsを入力して読み込まれたか確認します。
詳細
SwiftUI UI パターン
クイックスタート
既存のプロジェクト
- 機能と主要な操作モデル(一覧、詳細、エディター、設定、タブ型)を特定します
rg "TabView\("などを使って、リポジトリ内の近くにある実装例を探します- ローカルの規約に従います。SwiftUI ネイティブの状態管理を優先し、状態はローカルに保ちます
- 小さく、目的に絞ったサブビューと、SwiftUI ネイティブのデータフローを使って構築します
新規プロジェクトのスキャフォールディング
- TabView + NavigationStack + シートを接続します
- 最小限の
AppTabenum とRouterPathを追加します - 新しい画面を追加する際に、ルートとシートの enum を拡張します
一般的なルール
- 最新の SwiftUI の状態管理(
@State、@Binding、@Observable、@Environment)を使います - 不要なビューモデルは避け、MVVM より MV(Model-View)を優先します
- コンポジションを優先し、ビューを小さく目的に絞ったものに保ちます
.taskと明示的な読み込み状態・エラー状態を使って async/await を利用します- プロジェクトのフォーマッターとスタイルガイドに従います
シートのベストプラクティス
- 状態が選択されたモデルを表す場合は、
.sheet(isPresented:)より.sheet(item:)を優先します - シートの本体内で
if letを使わないようにします - シートは自身のアクションを管理し、内部で
dismiss()を呼び出します
新しい SwiftUI ビューのワークフロー
- ビューの状態と、その所有場所を定義します
@Environmentで注入する依存関係を特定します- ビュー階層を設計し、繰り返し使う部分をサブビューに切り出します
- 必要に応じて、
.taskと明示的な状態 enum を使って非同期読み込みを実装します - インタラクティブな UI にアクセシビリティラベルまたは識別子を追加します
- ビルドで検証し、必要に応じて使用箇所を更新します
MV パターン(MVVM より推奨)
SwiftUI のビューは、軽量な状態の表現にするべきです。本当に必要な場合を除き、ViewModel は避けます。
struct FeedView: View {
@Environment(APIClient.self) private var client
enum ViewState {
case loading
case error(String)
case loaded([Post])
}
@State private var viewState: ViewState = .loading
var body: some View {
NavigationStack {
List {
switch viewState {
case .loading:
ProgressView("Loading...")
case .error(let message):
ErrorView(message: message, retry: { await loadFeed() })
case .loaded(let posts):
ForEach(posts) { post in
PostRow(post: post)
}
}
}
.task { await loadFeed() }
}
}
private func loadFeed() async {
do {
let posts = try await client.getFeed()
viewState = .loaded(posts)
} catch {
viewState = .error(error.localizedDescription)
}
}
}
シートのパターン
項目駆動のシート(推奨)
@State private var selectedItem: Item?
.sheet(item: $selectedItem) { item in
EditItemSheet(item: item)
}
シートが自身のアクションを管理
struct EditItemSheet: View {
@Environment(\.dismiss) private var dismiss
@Environment(Store.self) private var store
let item: Item
@State private var isSaving = false
var body: some View {
VStack {
Button(isSaving ? "Saving..." : "Save") {
Task { await save() }
}
}
}
private func save() async {
isSaving = true
await store.save(item)
dismiss()
}
}
アプリレベルの環境設定
@main
struct MyApp: App {
@State var client: APIClient = .init()
@State var router: AppRouter = .init()
var body: some Scene {
WindowGroup {
TabView(selection: $router.selectedTab) {
ForEach(AppTab.allCases) { tab in
tab.rootView
.tag(tab)
}
}
.environment(client)
.environment(router)
}
}
}
状態管理のガイドライン
| ラッパー | 使用例 |
|---|---|
@State | ローカルで一時的なビューの状態 |
@Binding | 親からの双方向データフロー |
@Observable | ビュー間で共有する状態(iOS 17 以降) |
@Environment | 依存関係の注入、アプリ全体に関わる処理 |
@Query | ビュー内で直接行う SwiftData のクエリ |
Task と onChange のパターン
// React to state changes
.task(id: searchText) {
guard !searchText.isEmpty else { return }
await search(query: searchText)
}
// Respond to state transitions
.onChange(of: isActive, initial: false) {
guard isActive else { return }
Task { await refresh() }
}
MVVM を使わない理由
SwiftUI は、ViewModel を前提とせずに設計されています。
- ビューは構造体であり、軽量で破棄可能です
@State、@Environment、@Observableが、すべてのデータフローの要件を満たします- ViewModel は複雑さ、間接性、認知的負荷を増やします
- SwiftData の
@Queryはビュー内で直接使えます
代わりに:
- ビューは状態をそのまま表すものにします
@Environmentで注入するサービスやモデルにビジネスロジックを配置します- ビューではなく、サービスとモデルをテストします
- ビジュアルリグレッションテストには SwiftUI Previews を使います
コメント
Markdown 対応