mcpField
U
ui-patterns

ui-patterns

github.com/patrickserrano開発1366 回インストール42 週間前に更新
リポジトリ

ui-patterns について

SwiftUI アプリを開発・拡張するための UI パターンスキルです。ビュー構成、状態の所有箇所、ナビゲーションを案内し、SwiftUI 標準のデータフローと、操作を自身で管理するシートを重視します。

主な機能

  1. 01ビュー状態と所有箇所の設計
  2. 02小さく焦点を絞ったビューの構成
  3. 03SwiftUI の状態管理と環境注入
  4. 04非同期読み込みとエラー状態の処理
  5. 05既存プロジェクトの規約への準拠

活用シーン

  1. 01機能向けのリスト、詳細、編集画面の構築
  2. 02新規プロジェクトのタブとナビゲーションの設定
  3. 03シートと状態管理パターンの統一

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/patrickserrano/skills --skill ui-patterns

インストール後、Claude Code で/ui-patternsを入力して読み込まれたか確認します。

詳細

SwiftUI UI パターン

クイックスタート

既存のプロジェクト
  1. 機能と主要な操作モデル(一覧、詳細、エディター、設定、タブ型)を特定します
  2. rg "TabView\(" などを使って、リポジトリ内の近くにある実装例を探します
  3. ローカルの規約に従います。SwiftUI ネイティブの状態管理を優先し、状態はローカルに保ちます
  4. 小さく、目的に絞ったサブビューと、SwiftUI ネイティブのデータフローを使って構築します
新規プロジェクトのスキャフォールディング
  1. TabView + NavigationStack + シートを接続します
  2. 最小限の AppTab enum と RouterPath を追加します
  3. 新しい画面を追加する際に、ルートとシートの enum を拡張します

一般的なルール

  • 最新の SwiftUI の状態管理(@State、@Binding、@Observable、@Environment)を使います
  • 不要なビューモデルは避け、MVVM より MV(Model-View)を優先します
  • コンポジションを優先し、ビューを小さく目的に絞ったものに保ちます
  • .task と明示的な読み込み状態・エラー状態を使って async/await を利用します
  • プロジェクトのフォーマッターとスタイルガイドに従います
シートのベストプラクティス
  • 状態が選択されたモデルを表す場合は、.sheet(isPresented:) より .sheet(item:) を優先します
  • シートの本体内で if let を使わないようにします
  • シートは自身のアクションを管理し、内部で dismiss() を呼び出します

新しい SwiftUI ビューのワークフロー

  1. ビューの状態と、その所有場所を定義します
  2. @Environment で注入する依存関係を特定します
  3. ビュー階層を設計し、繰り返し使う部分をサブビューに切り出します
  4. 必要に応じて、.task と明示的な状態 enum を使って非同期読み込みを実装します
  5. インタラクティブな UI にアクセシビリティラベルまたは識別子を追加します
  6. ビルドで検証し、必要に応じて使用箇所を更新します

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 を使います

コメント