Skip to main content

swiftui-preview-convergence

Architecture pattern where one scenario system powers SwiftUI previews, tests, and production. Use when creating or refactoring views, state objects, mocks, fixtures, or dependency injection. Forces clean separation by making previews the design linter. macOS 26+, iOS 26+, Swift 6.

ソース情報

リポジトリ
JordanCoin/swiftui-preview-convergence
ソースの最終更新活動
2026年4月4日 15:29
検出された SKILL.md の言語
英語
スター
0
フォーク
0

インストール方法

デフォルトでは、最初にソースを確認する Prompt が選択されています。直接コマンドに切り替えるか、ローカルコピーをダウンロードすることもできます。

ソースファイルを確認

インストールを決める前に、SKILL.md と SkillsMP に表示されている付属ファイルをお読みください。

SKILL.md を表示中

SKILL.md
ソースの指示 · 読み取り専用プレビュー
name
swiftui-preview-convergence
description
Architecture pattern where one scenario system powers SwiftUI previews, tests, and production. Use when creating or refactoring views, state objects, mocks, fixtures, or dependency injection. Forces clean separation by making previews the design linter. macOS 26+, iOS 26+, Swift 6.
# SwiftUI Preview/Test Convergence Build one scenario system that powers previews, tests, and production. > If previews are hard to build, the architecture is wrong. ## Outcome Each feature has: - A `View` (renders state, triggers intent) - An `@Observable` state object - A service `protocol` - A live implementation - A scenario-driven mock - Centralized fixtures - Shared constructors for previews and tests The same scenario language works everywhere: production, previews, tests. ## Rules ### 1. Views are dumb Views render state and trigger intent. Nothing else. Allowed: - State rendering - User interaction - `.task` lifecycle triggers - Lightweight composition Not allowed: - API calls - Business logic - Parsing - Hidden dependency creation ### 2. State objects depend on protocols ```swift protocol ItemServicing: Sendable { func fetchItems() async throws -> [Item] } ``` The state object depends on `ItemServicing`, never on `URLSession` or a concrete SDK. ### 3. Mocks are scenarios, not placeholders Bad: ```swift final class MockItemService: ItemServicing { var items: [Item] = [] func fetchItems() async throws -> [Item] { items } } ``` Good: ```swift final class MockItemService: ItemServicing, Sendable { enum Scenario: Sendable { case success([Item]) case empty case failure(Error) case delayed([Item], duration: Duration = .seconds(1)) } private let scenario: Scenario init(_ scenario: Scenario) { self.scenario = scenario } func fetchItems() async throws -> [Item] { switch scenario { case .success(let items): return items case .empty: return [] case .failure(let error): throw error case .delayed(let items, let duration): try await Task.sleep(for: duration) return items } } } ``` ### 4. Centralize fixtures ```swift enum ItemFixtures { static let one = Item( id: UUID(uuidString: "11111111-1111-1111-1111-111111111111")!, title: "First Item" ) static let two = Item( id: UUID(uuidString: "22222222-2222-2222-2222-222222222222")!, title: "Second Item" ) static let list = [one, two] } ``` Deterministic. Readable names. Zero duplicated fake setup. ### 5. Shared constructors — the convergence point ```swift enum ItemScenarios { @MainActor static func loaded() -> ItemsState { ItemsState(service: MockItemService(.success(ItemFixtures.list))) } @MainActor static func empty() -> ItemsState { ItemsState(service: MockItemService(.empty)) } @MainActor static func error() -> ItemsState { ItemsState(service: MockItemService(.failure(AppError("Something went wrong")))) } @MainActor static func loading() -> ItemsState { ItemsState(service: MockItemService(.delayed(ItemFixtures.list))) } } ``` Same constructors in previews, tests, and dev exploration. ### 6. Previews are your design linter If previews are hard to build, the feature is too coupled. Smells: - Preview requires real networking - Preview needs app-wide state - Preview breaks without a singleton - Preview setup is longer than the feature ### 7. One scenario vocabulary Use the same names everywhere: `loaded`, `empty`, `error`, `loading`, `delayed`, `partial`, `unauthorized`. Do not invent separate setup languages for previews and tests. ## Canonical Template (Swift 6, macOS 26+) ### Model ```swift struct Item: Identifiable, Equatable, Sendable { let id: UUID let title: String } ``` ### Protocol ```swift protocol ItemServicing: Sendable { func fetchItems() async throws -> [Item] } ``` ### Live service ```swift struct LiveItemService: ItemServicing { func fetchItems() async throws -> [Item] { // real implementation [] } } ``` ### Mock service ```swift final class MockItemService: ItemServicing, Sendable { enum Scenario: Sendable { case success([Item]) case empty case failure(Error) case delayed([Item], duration: Duration = .seconds(1)) } private let scenario: Scenario init(_ scenario: Scenario) { self.scenario = scenario } func fetchItems() async throws -> [Item] { switch scenario { case .success(let items): return items case .empty: return [] case .failure(let error): throw error case .delayed(let items, let duration): try await Task.sleep(for: duration) return items } } } ``` ### State object ```swift @Observable @MainActor final class ItemsState { enum ViewState: Equatable { case idle, loading case loaded([Item]) case empty case error(String) } private(set) var viewState: ViewState = .idle private let service: ItemServicing private var hasLoaded = false init(service: ItemServicing) { self.service = service } func loadIfNeeded() async { guard !hasLoaded else { return } hasLoaded = true await load() } func load() async { viewState = .loading do { let items = try await service.fetchItems() viewState = items.isEmpty ? .empty : .loaded(items) } catch { viewState = .error(error.localizedDescription) } } } ``` ### Scenarios (shared constructors) ```swift enum ItemScenarios { @MainActor static func loaded() -> ItemsState { ItemsState(service: MockItemService(.success(ItemFixtures.list))) } @MainActor static func empty() -> ItemsState { ItemsState(service: MockItemService(.empty)) } @MainActor static func error() -> ItemsState { ItemsState(service: MockItemService(.failure(AppError("Something went wrong")))) } @MainActor static func loading() -> ItemsState { ItemsState(service: MockItemService(.delayed(ItemFixtures.list))) } } struct AppError: LocalizedError { let message: String init(_ message: String) { self.message = message } var errorDescription: String? { message } } ``` ### View ```swift struct ItemsView: View { @State private var state: ItemsState init(state: ItemsState) { _state = State(initialValue: state) } var body: some View { content .task { await state.loadIfNeeded() } } @ViewBuilder private var content: some View { switch state.viewState { case .idle, .loading: ProgressView("Loading...") case .loaded(let items): List(items) { item in Text(item.title) } case .empty: ContentUnavailableView("No Items", systemImage: "tray") case .error(let message): ContentUnavailableView("Error", systemImage: "exclamationmark.triangle", description: Text(message)) } } } ``` ### Previews ```swift #Preview("Loaded") { ItemsView(state: ItemScenarios.loaded()) } #Preview("Empty") { ItemsView(state: ItemScenarios.empty()) } #Preview("Error") { ItemsView(state: ItemScenarios.error()) } #Preview("Loading") { ItemsView(state: ItemScenarios.loading()) } ``` ### Tests (Swift Testing) ```swift import Testing struct ItemsStateTests { @Test @MainActor func loadedState() async { let state = ItemScenarios.loaded() await state.load() #expect(state.viewState == .loaded(ItemFixtures.list)) } @Test @MainActor func emptyState() async { let state = ItemScenarios.empty() await state.load() #expect(state.viewState == .empty) } @Test @MainActor func errorState() async { let state = ItemScenarios.error() await state.load() #expect(state.viewState == .error("Something went wrong")) } } ``` ## Folder shape ``` Features/ Items/ ItemsView.swift ItemsState.swift ItemServicing.swift LiveItemService.swift MockItemService.swift ItemFixtures.swift ItemScenarios.swift ItemsStateTests.swift ``` ## Quality bar A feature is correct when: - The view renders without constructing dependencies - The state object depends only on protocols - Mocks model behavior through scenarios - Previews need zero networking or app bootstrap - Tests use the same constructors as previews - Fixtures are deterministic and reusable - Scenario names map to actual UI states ## Fix immediately - Service creation inside a view - `Task` doing real work in the view body - Previews using live API clients - Tests building ad-hoc mocks divergent from previews - Fake data duplicated across files - State objects depending on concrete SDK clients - Singletons required to render a feature ## Principle Don't think "I need a mock for tests." Think "I need a scenario system for this feature." That system constructs service behavior, state transitions, preview states, and test states. That is the pattern. That is the convergence.
GitHubで見る