AgentSkillsCN

swiftui-screen

当您创作视觉设计、动画,以及交互模式时,可使用此技能。适用于设计系统、Material 3,以及无障碍设计。

SKILL.md
--- frontmatter
name: swiftui-screen
description: This skill should be used when building native iOS UI with SwiftUI backed by KMP ViewModels. Use for iOS screens, StateFlow bridging, and iOS lifecycle management.

SwiftUI Screen Skill

Expert guidance for implementing native iOS SwiftUI screens backed by shared Kotlin Multiplatform ViewModels. Focus on SwiftUI integration, StateFlow bridging, and iOS lifecycle management.

When to Use

Use this skill when:

  • Implementing SwiftUI screens consuming KMP ViewModels
  • Bridging StateFlow to SwiftUI with @State properties
  • Setting up iOS lifecycle management (onAppear/onDisappear)
  • Creating SwiftUI Previews with mock ViewModels
  • Working with parametric ViewModels (pokemonId, userId)
  • Using SKIE for Kotlin-Swift interop
  • Deciding between Direct Integration vs Wrapper pattern

Do NOT use this skill for:

  • Shared ViewModel implementation → switch to KMP Mobile Expert Mode
  • Compose UI implementation → switch to Compose Screen Implementation Mode
  • Product/PRD decisions → switch to Product Design Mode
  • Visual design/animations → switch to UI/UX Design Mode

Mode Detection

User RequestModeContext
"Create a SwiftUI screen for..."IMPLEMENTATION_MODEBuilding new iOS UI
"How do I observe StateFlow?"STATEFLOW_MODEBridging Kotlin flows to SwiftUI
"My ViewModel isn't updating the UI"DEBUG_MODETroubleshooting StateFlow bridging
"Should I use @StateObject or @ObservedObject?"PATTERN_MODESwiftUI lifecycle decisions
"How do I pass pokemonId to ViewModel?"PARAMETRIC_MODEParametric ViewModels
"Create a preview for this SwiftUI view"PREVIEW_MODESwiftUI Previews

Essential Workflows

Workflow 1: Direct Integration (Current Pattern)

Use Direct Integration for simple to medium complexity apps with linear navigation:

  1. Create SwiftUI View with Direct ViewModel Access

    swift
    import SwiftUI
    import Shared
    
    struct PokemonListView: View {
        // Direct ViewModel access from Koin
        private var viewModel = KoinIosKt.getPokemonListViewModel()
    
        // @State bridges StateFlow to SwiftUI
        @State private var uiState: PokemonListUiState = PokemonListUiStateLoading()
    
        var body: some View {
            content
                .onAppear {
                    // Load data on first appear
                    if case is PokemonListUiStateLoading = uiState {
                        viewModel.loadInitialPage()
                    }
                }
                .task {
                    // Observe StateFlow - auto-cancels on view disappear
                    for await state in viewModel.uiState {
                        self.uiState = state
                    }
                }
        }
    }
    
  2. Handle UI States with Switch

    swift
    @ViewBuilder
    private var content: some View {
        switch uiState {
        case is PokemonListUiStateLoading:
            ProgressView("Loading...")
    
        case let error as PokemonListUiStateError:
            ErrorView(message: error.message) {
                viewModel.loadInitialPage() // Retry
            }
    
        case let content as PokemonListUiStateContent:
            PokemonListGrid(
                pokemons: content.pokemons,
                onLoadMore: { viewModel.loadNextPage() }
            )
    
        default:
            EmptyView()
        }
    }
    
  3. Test and Validate

    • Verify StateFlow updates SwiftUI @State correctly
    • Test loading, error, and content states
    • Confirm .task cancels on view disappear (no memory leaks)
    • Run iOS build in Xcode: open iosApp/iosApp.xcodeproj

Workflow 2: Parametric ViewModels

Use when ViewModels require constructor parameters (e.g., pokemonId, userId):

  1. Create Koin Helper with Parameters

    kotlin
    // shared/src/iosMain/kotlin/KoinIos.kt
    fun getPokemonDetailViewModel(pokemonId: Int): PokemonDetailViewModel {
        return KoinPlatform.getKoin().get { parametersOf(pokemonId) }
    }
    
  2. Create SwiftUI View with Initialization

    swift
    import SwiftUI
    import Shared
    
    struct PokemonDetailView: View {
        let pokemonId: Int
        private var viewModel: PokemonDetailViewModel
    
        @State private var uiState: PokemonDetailUiState = PokemonDetailUiStateLoading()
    
        init(pokemonId: Int) {
            self.pokemonId = pokemonId
            // Get parametric ViewModel from Koin
            // Cast Swift Int to Kotlin Int32
            viewModel = KoinIosKt.getPokemonDetailViewModel(pokemonId: Int32(pokemonId))
        }
    
        var body: some View {
            content
                .task {
                    for await state in viewModel.uiState {
                        self.uiState = state
                    }
                }
        }
    }
    
  3. Handle Type Conversions

    swift
    // Kotlin Int32 → Swift Int conversion required
    String(format: "#%03d", Int(pokemon.id))  // pokemon.id is Int32
    String(format: "%.1f m", Double(pokemon.height) / 10.0)
    

Workflow 3: SwiftUI Previews with Mock Data

Create previews for all UI states to enable visual testing:

  1. Create Mock ViewModel Helper

    swift
    // iosApp/Previews/MockViewModels.swift
    import SwiftUI
    import Shared
    
    class MockPokemonListViewModel: ObservableObject {
        @Published var uiState: PokemonListUiState = PokemonListUiStateLoading()
    
        init(state: PokemonListUiState) {
            self.uiState = state
        }
    }
    
  2. Create Preview Variants

    swift
    #Preview("Loading") {
        PokemonListView_Previews.LoadingPreview()
    }
    
    #Preview("Error") {
        PokemonListView_Previews.ErrorPreview()
    }
    
    #Preview("Content") {
        PokemonListView_Previews.ContentPreview()
    }
    
    extension PokemonListView_Previews {
        static func LoadingPreview() -> some View {
            PokemonListView()
                .environment(\.mockViewModel, MockPokemonListViewModel(
                    state: PokemonListUiStateLoading()
                ))
        }
    
        static func ErrorPreview() -> some View {
            PokemonListView()
                .environment(\.mockViewModel, MockPokemonListViewModel(
                    state: PokemonListUiStateError(message: "Network error")
                ))
        }
    
        static func ContentPreview() -> some View {
            PokemonListView()
                .environment(\.mockViewModel, MockPokemonListViewModel(
                    state: PokemonListUiStateContent(
                        pokemons: persistentListOf(
                            Pokemon(name: "Bulbasaur", detailUrl: "..."),
                            Pokemon(name: "Charmander", detailUrl: "...")
                        ),
                        hasMore: true
                    )
                ))
        }
    }
    
  3. Validate Previews in Xcode

    • Open Xcode: open iosApp/iosApp.xcodeproj
    • Navigate to the SwiftUI file
    • Verify all preview states render correctly
    • Check for layout issues at different device sizes

Critical Guardrails

RuleWhy It MattersHow to Apply
Always use @StateObject for wrapper, @State for ViewModel state@StateObject preserves ViewModel across View recreations; @State bridges StateFlow to SwiftUIUse @StateObject private var wrapper = ViewModelWrapper() for wrappers; use @State private var uiState = Loading() for StateFlow bridging
Observe StateFlow in .task, not .onAppear.task automatically cancels AsyncSequence on view disappear, preventing memory leaksUse .task { for await state in viewModel.uiState { self.uiState = state } }
Cast Kotlin Int32 to Swift IntKotlin's Int maps to Swift's Int32, not IntAlways cast: Int(pokemon.id), String(format: "%03d", Int(pokemon.id))
Use String(format:) for formatted outputSwift string interpolation doesn't support format specifiersUse String(format: "%.1f m", value) not "\(value:.1f) m"
Check for SKIE-renamed typesKotlin classes named after Swift keywords get _ suffixLook for Type_, Error_, Result_ instead of Type, Error, Result
Import Shared frameworkAll KMP exports are available via Shared frameworkAlways add import Shared at top of SwiftUI files
Never export :data or :wiring to iOSThese are internal KMP modules; iOS only needs :api and :presentationCheck :shared/build.gradle.kts export list - only :api and :presentation should be exported

Quick Reference

Key Patterns

PatternCode SnippetWhen to Use
Direct ViewModel Accessprivate var viewModel = KoinIosKt.getPokemonListViewModel()Simple screens, non-parametric
Parametric ViewModelviewModel = KoinIosKt.getPokemonDetailViewModel(pokemonId: Int32(id))Screens requiring constructor params
StateFlow Bridging@State private var uiState = Loading() + .task { for await state in viewModel.uiState { self.uiState = state } }Observing StateFlow in SwiftUI
UI State Switchswitch uiState { case is Loading: ProgressView() case let content as Content: ... }Rendering sealed class states
Type CastingInt(pokemon.id)Kotlin Int32 → Swift Int
String FormattingString(format: "%.1f m", value)Formatted numeric output
SKIE Renamed Typeslet type: Type_ = pokemon.types.first!Swift keyword collision types

Type Conversion Cheat Sheet

Kotlin TypeSwift TypeExample
IntInt32let id = Int32(pokemon.id)
StringStringDirect, no conversion needed
DoubleDoubleDirect, no conversion needed
BooleanBoolDirect, no conversion needed
List<T>KotlinArray<T>Use Array(kotlinArray) to convert
StateFlow<T>AsyncSequence<T>Use .task { for await state in flow { ... } }

Common SKIE Renamed Types

Kotlin ClassSwift Name (after SKIE)
TypeType_
ErrorError_
ResultResult_
SelfSelf_
ProtocolProtocol_

Validation Commands

CommandPurposeWhen to Run
bash -n .claude/skills/swiftui-screen/scripts/validate-swiftui.shValidate script syntaxAfter creating validation script
./gradlew :composeApp:assembleDebug test --continuePrimary validation (Android + tests)Before committing
open iosApp/iosApp.xcodeprojOpen iOS app in XcodeWhen working on iOS features
./gradlew :shared:embedAndSignAppleFrameworkForXcodeBuild iOS frameworkBefore iOS builds

Cross-References

Documentation Links

DocumentPurposeLocation
iOS Integration GuideComplete iOS + KMP integration detailsdocs/tech/ios_integration.md
Architecture + ConventionsMaster architecture referencedocs/tech/conventions.md
Critical Patterns6 core patterns (ViewModel, Either, etc.)docs/tech/critical_patterns_quick_ref.md
Product RequirementsFeature acceptance criteriadocs/project/prd.md
Testing StrategyTest coverage and patternsdocs/tech/testing_strategy.md

Related Skills

SkillPurposeWhen to Switch
kmp-mobile-expertShared ViewModel, repository, and iOS bridging implementationImplementing shared business logic
compose-screenCompose Multiplatform UI implementation (Android/Desktop)Building Compose screens
ui-ux-designerVisual design and animationsCreating custom animations or design systems

Reference Implementations

FeatureFilesPattern
Pokemon ListiosApp/iosApp/Views/PokemonListView.swiftDirect Integration
Pokemon DetailiosApp/iosApp/Views/PokemonDetailView.swiftParametric ViewModel
Pokemon List GridiosApp/iosApp/Views/PokemonListGrid.swiftGrid layout with infinite scroll
Type BadgesiosApp/iosApp/Views/TypeBadge.swiftCustom view component

Decision Guides

When to Use Direct Integration vs Wrapper Pattern:

RequirementDirect IntegrationWrapper Pattern
Simple linear navigation✅ Recommended⚠️ Overkill
Tab-based navigation❌ State loss on tab switch✅ Required
Sheet/modal presentation⚠️ May lose state✅ Preserves state
Parent view has @State❌ ViewModel recreated✅ Survives
Deep navigation stacks⚠️ Fragile✅ Robust
Team new to SwiftUI✅ Simpler to understand❌ More complex
Testing in Previews❌ Hard to mock✅ Easy to mock
Boilerplate tolerance✅ Low (~10 lines)❌ High (~80 lines)
Production large-scale app⚠️ Risky for complex flows✅ Safer choice
MVP/POC project✅ Fast iteration⚠️ Premature optimization

See ios_integration.md for complete comparison and migration guide.