swiftui-agent-skill

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

SwiftUI Agent Skill

SwiftUI Agent Skill

Skill by ara.so — AI Agent Skills collection.
An agent skill that helps AI coding assistants write smarter, simpler, and more modern SwiftUI code. Provides guidance on API usage, design patterns, performance optimization, and accessibility. Covers navigation, layout, animations, state management, VoiceOver, deprecated APIs, and common LLM mistakes.
ara.so开发的技能——AI Agent技能合集。
这是一款Agent技能,可帮助AI编码助手编写更智能、更简洁、更现代化的SwiftUI代码。提供API使用、设计模式、性能优化和无障碍访问方面的指导,涵盖导航、布局、动画、状态管理、VoiceOver、废弃API以及LLM常见错误等内容。

Installation

安装

Install via npx:
bash
npx skills add https://github.com/twostraws/swiftui-agent-skill --skill swiftui-pro
Or via Claude Code marketplace:
/plugin marketplace add twostraws/SwiftUI-Agent-Skill
/plugin install swiftui-pro@swiftui-agent-skill
通过npx安装:
bash
npx skills add https://github.com/twostraws/swiftui-agent-skill --skill swiftui-pro
或通过Claude Code应用市场安装:
/plugin marketplace add twostraws/SwiftUI-Agent-Skill
/plugin install swiftui-pro@swiftui-agent-skill

Usage

使用方法

In Claude Code

在Claude Code中使用

/swiftui-pro
With specific focus:
/swiftui-pro Check for deprecated API
/swiftui-pro Focus on accessibility
/swiftui-pro Review navigation patterns
/swiftui-pro
指定重点方向:
/swiftui-pro Check for deprecated API
/swiftui-pro Focus on accessibility
/swiftui-pro Review navigation patterns

In Codex

在Codex中使用

$swiftui-pro
With specific instructions:
$swiftui-pro Look for performance issues
$swiftui-pro Check VoiceOver support
$swiftui-pro
指定具体指令:
$swiftui-pro Look for performance issues
$swiftui-pro Check VoiceOver support

Natural Language

自然语言指令

Use the SwiftUI Pro skill to review this view for best practices
Check this SwiftUI code with the agent skill
Use the SwiftUI Pro skill to review this view for best practices
Check this SwiftUI code with the agent skill

Key SwiftUI Best Practices

SwiftUI核心最佳实践

Modern Navigation

现代化导航

Use NavigationStack and NavigationPath (iOS 16+), not deprecated NavigationView:
swift
struct ContentView: View {
    @State private var path = NavigationPath()
    
    var body: some View {
        NavigationStack(path: $path) {
            List {
                NavigationLink("Detail", value: "detail")
            }
            .navigationDestination(for: String.self) { value in
                DetailView(item: value)
            }
        }
    }
}
Programmatic navigation:
swift
struct ContentView: View {
    @State private var path = NavigationPath()
    
    var body: some View {
        NavigationStack(path: $path) {
            Button("Go to Detail") {
                path.append("detail")
            }
            .navigationDestination(for: String.self) { value in
                DetailView(item: value)
            }
        }
    }
}
使用NavigationStack和NavigationPath(iOS 16+),替代已废弃的NavigationView:
swift
struct ContentView: View {
    @State private var path = NavigationPath()
    
    var body: some View {
        NavigationStack(path: $path) {
            List {
                NavigationLink("Detail", value: "detail")
            }
            .navigationDestination(for: String.self) { value in
                DetailView(item: value)
            }
        }
    }
}
程序化导航:
swift
struct ContentView: View {
    @State private var path = NavigationPath()
    
    var body: some View {
        NavigationStack(path: $path) {
            Button("Go to Detail") {
                path.append("detail")
            }
            .navigationDestination(for: String.self) { value in
                DetailView(item: value)
            }
        }
    }
}

State Management

状态管理

Use @State for view-local state:
swift
struct CounterView: View {
    @State private var count = 0
    
    var body: some View {
        Button("Count: \(count)") {
            count += 1
        }
    }
}
Use @Observable for shared state (iOS 17+), not ObservableObject:
swift
@Observable
class AppState {
    var username = ""
    var isLoggedIn = false
}

struct ContentView: View {
    let state = AppState()
    
    var body: some View {
        Text("User: \(state.username)")
            .onChange(of: state.isLoggedIn) { oldValue, newValue in
                print("Login state changed")
            }
    }
}
Use @Bindable for bindings to Observable objects:
swift
struct ProfileView: View {
    @Bindable var user: User
    
    var body: some View {
        TextField("Name", text: $user.name)
    }
}
使用@State管理视图本地状态:
swift
struct CounterView: View {
    @State private var count = 0
    
    var body: some View {
        Button("Count: \(count)") {
            count += 1
        }
    }
}
使用@Observable管理共享状态(iOS 17+),替代ObservableObject:
swift
@Observable
class AppState {
    var username = ""
    var isLoggedIn = false
}

struct ContentView: View {
    let state = AppState()
    
    var body: some View {
        Text("User: \(state.username)")
            .onChange(of: state.isLoggedIn) { oldValue, newValue in
                print("Login state changed")
            }
    }
}
使用@Bindable绑定Observable对象:
swift
struct ProfileView: View {
    @Bindable var user: User
    
    var body: some View {
        TextField("Name", text: $user.name)
    }
}

Accessibility

无障碍访问

Always provide accessibility labels for custom controls:
swift
struct CustomButton: View {
    var body: some View {
        Image(systemName: "star.fill")
            .foregroundStyle(.yellow)
            .onTapGesture {
                // action
            }
            .accessibilityLabel("Favorite")
            .accessibilityAddTraits(.isButton)
    }
}
Use accessibilityElement for grouped content:
swift
struct UserCard: View {
    let name: String
    let age: Int
    
    var body: some View {
        VStack {
            Text(name)
            Text("\(age) years old")
        }
        .accessibilityElement(children: .combine)
    }
}
Provide hints for complex interactions:
swift
Button("Delete") {
    // delete action
}
.accessibilityHint("Deletes this item permanently")
始终为自定义控件提供无障碍标签:
swift
struct CustomButton: View {
    var body: some View {
        Image(systemName: "star.fill")
            .foregroundStyle(.yellow)
            .onTapGesture {
                // action
            }
            .accessibilityLabel("Favorite")
            .accessibilityAddTraits(.isButton)
    }
}
使用accessibilityElement组合内容:
swift
struct UserCard: View {
    let name: String
    let age: Int
    
    var body: some View {
        VStack {
            Text(name)
            Text("\(age) years old")
        }
        .accessibilityElement(children: .combine)
    }
}
为复杂交互提供提示:
swift
Button("Delete") {
    // delete action
}
.accessibilityHint("Deletes this item permanently")

Layout

布局

Use modern layout containers:
swift
// Grid for structured layouts
Grid(alignment: .leading, horizontalSpacing: 20) {
    GridRow {
        Text("Name:")
        Text("John")
    }
    GridRow {
        Text("Age:")
        Text("30")
    }
}

// ViewThatFits for responsive layouts
ViewThatFits {
    HStack {
        content
    }
    VStack {
        content
    }
}
Avoid nested geometry readers:
swift
// BAD
GeometryReader { outer in
    GeometryReader { inner in
        // layout code
    }
}

// GOOD - use .containerRelativeFrame or layout protocol
Color.blue
    .containerRelativeFrame(.horizontal) { width, axis in
        width * 0.5
    }
使用现代化布局容器:
swift
// Grid用于结构化布局
Grid(alignment: .leading, horizontalSpacing: 20) {
    GridRow {
        Text("Name:")
        Text("John")
    }
    GridRow {
        Text("Age:")
        Text("30")
    }
}

// ViewThatFits用于响应式布局
ViewThatFits {
    HStack {
        content
    }
    VStack {
        content
    }
}
避免嵌套GeometryReader:
swift
// 不良写法
GeometryReader { outer in
    GeometryReader { inner in
        // layout code
    }
}

// 推荐写法 - 使用.containerRelativeFrame或布局协议
Color.blue
    .containerRelativeFrame(.horizontal) { width, axis in
        width * 0.5
    }

Performance

性能优化

Use lazy containers for large lists:
swift
// Always use LazyVStack/LazyHStack for scrolling content
ScrollView {
    LazyVStack {
        ForEach(items) { item in
            ItemView(item: item)
        }
    }
}
Avoid expensive computations in body:
swift
struct ContentView: View {
    @State private var items: [Item] = []
    
    // BAD - computed every time body runs
    var body: some View {
        let sortedItems = items.sorted()
        List(sortedItems) { item in
            Text(item.name)
        }
    }
}

// GOOD - cache computed values
struct ContentView: View {
    @State private var items: [Item] = []
    
    private var sortedItems: [Item] {
        items.sorted()
    }
    
    var body: some View {
        List(sortedItems) { item in
            Text(item.name)
        }
    }
}
Use task modifiers instead of onAppear for async work:
swift
// GOOD
struct ContentView: View {
    @State private var data: [Item] = []
    
    var body: some View {
        List(data) { item in
            Text(item.name)
        }
        .task {
            await loadData()
        }
    }
    
    func loadData() async {
        // async loading
    }
}
为大型列表使用懒加载容器:
swift
// 滚动内容始终使用LazyVStack/LazyHStack
ScrollView {
    LazyVStack {
        ForEach(items) { item in
            ItemView(item: item)
        }
    }
}
避免在body中执行昂贵计算:
swift
struct ContentView: View {
    @State private var items: [Item] = []
    
    // 不良写法 - 每次body运行时都会重新计算
    var body: some View {
        let sortedItems = items.sorted()
        List(sortedItems) { item in
            Text(item.name)
        }
    }
}

// 推荐写法 - 缓存计算结果
struct ContentView: View {
    @State private var items: [Item] = []
    
    private var sortedItems: [Item] {
        items.sorted()
    }
    
    var body: some View {
        List(sortedItems) { item in
            Text(item.name)
        }
    }
}
使用task修饰符替代onAppear处理异步任务:
swift
// 推荐写法
struct ContentView: View {
    @State private var data: [Item] = []
    
    var body: some View {
        List(data) { item in
            Text(item.name)
        }
        .task {
            await loadData()
        }
    }
    
    func loadData() async {
        // async loading
    }
}

Animations

动画

Use withAnimation for state-driven animations:
swift
struct AnimatedView: View {
    @State private var isExpanded = false
    
    var body: some View {
        VStack {
            if isExpanded {
                Text("Details")
                    .transition(.move(edge: .top))
            }
            
            Button("Toggle") {
                withAnimation(.spring(response: 0.3)) {
                    isExpanded.toggle()
                }
            }
        }
    }
}
Use animation modifier for continuous animations:
swift
struct RotatingView: View {
    @State private var rotation = 0.0
    
    var body: some View {
        Image(systemName: "arrow.clockwise")
            .rotationEffect(.degrees(rotation))
            .animation(.linear(duration: 1).repeatForever(autoreverses: false), value: rotation)
            .onAppear {
                rotation = 360
            }
    }
}
使用withAnimation实现状态驱动动画:
swift
struct AnimatedView: View {
    @State private var isExpanded = false
    
    var body: some View {
        VStack {
            if isExpanded {
                Text("Details")
                    .transition(.move(edge: .top))
            }
            
            Button("Toggle") {
                withAnimation(.spring(response: 0.3)) {
                    isExpanded.toggle()
                }
            }
        }
    }
}
使用animation修饰符实现连续动画:
swift
struct RotatingView: View {
    @State private var rotation = 0.0
    
    var body: some View {
        Image(systemName: "arrow.clockwise")
            .rotationEffect(.degrees(rotation))
            .animation(.linear(duration: 1).repeatForever(autoreverses: false), value: rotation)
            .onAppear {
                rotation = 360
            }
    }
}

Common Deprecated APIs to Avoid

需避免的常见废弃API

NavigationView → Use
NavigationStack
sheet(isPresented:onDismiss:content:) with @State bool is fine, but for complex flows use
NavigationStack
GeometryReader for sizing → Use
containerRelativeFrame
or layout protocol
ObservableObject → Use
@Observable
macro (iOS 17+)
@Published → Not needed with
@Observable
@StateObject → Use
@State
with
@Observable
objects
onChange(of:perform:) → Use
onChange(of:initial:_:)
with old/new values
NavigationView → 使用
NavigationStack
**sheet(isPresented:onDismiss:content:)**搭配@State布尔值是可行的,但复杂流程建议使用
NavigationStack
GeometryReader用于尺寸设置 → 使用
containerRelativeFrame
或布局协议
ObservableObject → 使用
@Observable
宏(iOS 17+)
@Published → 使用
@Observable
后不再需要
@StateObject → 使用
@State
搭配
@Observable
对象
onChange(of:perform:) → 使用包含新旧值的
onChange(of:initial:_:)

Configuration

配置

Project-wide Installation

全局安装

Install for all projects:
bash
npx skills add https://github.com/twostraws/swiftui-agent-skill --skill swiftui-pro
为所有项目安装:
bash
npx skills add https://github.com/twostraws/swiftui-agent-skill --skill swiftui-pro

Select "all projects" during installation

安装时选择"all projects"

undefined
undefined

Project-specific Installation

项目专属安装

Install for current project only:
bash
npx skills add https://github.com/twostraws/swiftui-agent-skill --skill swiftui-pro
仅为当前项目安装:
bash
npx skills add https://github.com/twostraws/swiftui-agent-skill --skill swiftui-pro

Select "this project only" during installation

安装时选择"this project only"

undefined
undefined

Common Patterns

常见模式

Form with Validation

带验证的表单

swift
@Observable
class FormData {
    var email = ""
    var password = ""
    
    var isValid: Bool {
        !email.isEmpty && password.count >= 8
    }
}

struct LoginForm: View {
    @State private var formData = FormData()
    
    var body: some View {
        Form {
            TextField("Email", text: $formData.email)
                .textContentType(.emailAddress)
                .keyboardType(.emailAddress)
            
            SecureField("Password", text: $formData.password)
                .textContentType(.password)
            
            Button("Login") {
                // login action
            }
            .disabled(!formData.isValid)
        }
    }
}
swift
@Observable
class FormData {
    var email = ""
    var password = ""
    
    var isValid: Bool {
        !email.isEmpty && password.count >= 8
    }
}

struct LoginForm: View {
    @State private var formData = FormData()
    
    var body: some View {
        Form {
            TextField("Email", text: $formData.email)
                .textContentType(.emailAddress)
                .keyboardType(.emailAddress)
            
            SecureField("Password", text: $formData.password)
                .textContentType(.password)
            
            Button("Login") {
                // login action
            }
            .disabled(!formData.isValid)
        }
    }
}

Environment Injection

环境注入

swift
@Observable
class AppSettings {
    var theme = "light"
    var fontSize = 14.0
}

@main
struct MyApp: App {
    @State private var settings = AppSettings()
    
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environment(settings)
        }
    }
}

struct ContentView: View {
    @Environment(AppSettings.self) private var settings
    
    var body: some View {
        Text("Theme: \(settings.theme)")
    }
}
swift
@Observable
class AppSettings {
    var theme = "light"
    var fontSize = 14.0
}

@main
struct MyApp: App {
    @State private var settings = AppSettings()
    
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environment(settings)
        }
    }
}

struct ContentView: View {
    @Environment(AppSettings.self) private var settings
    
    var body: some View {
        Text("Theme: \(settings.theme)")
    }
}

Custom View Modifiers

自定义视图修饰符

swift
struct CardStyle: ViewModifier {
    func body(content: Content) -> some View {
        content
            .padding()
            .background(.background.secondary)
            .clipShape(RoundedRectangle(cornerRadius: 12))
            .shadow(radius: 2)
    }
}

extension View {
    func cardStyle() -> some View {
        modifier(CardStyle())
    }
}

// Usage
Text("Hello")
    .cardStyle()
swift
struct CardStyle: ViewModifier {
    func body(content: Content) -> some View {
        content
            .padding()
            .background(.background.secondary)
            .clipShape(RoundedRectangle(cornerRadius: 12))
            .shadow(radius: 2)
    }
}

extension View {
    func cardStyle() -> some View {
        modifier(CardStyle())
    }
}

// 使用示例
Text("Hello")
    .cardStyle()

Troubleshooting

故障排查

"Purple warnings" in console

控制台出现“紫色警告”

Usually caused by state updates during view updates. Use
Task
or
DispatchQueue.main.async
:
swift
.onAppear {
    Task {
        await loadData()
    }
}
通常是由于视图更新期间修改状态导致的。使用
Task
DispatchQueue.main.async
swift
.onAppear {
    Task {
        await loadData()
    }
}

Views not updating

视图未更新

Ensure Observable objects are properly injected:
swift
// BAD
struct ChildView: View {
    let settings: AppSettings // Won't observe changes
}

// GOOD
struct ChildView: View {
    @Environment(AppSettings.self) private var settings
}
确保Observable对象已正确注入:
swift
// 不良写法
struct ChildView: View {
    let settings: AppSettings // 无法监听变化
}

// 推荐写法
struct ChildView: View {
    @Environment(AppSettings.self) private var settings
}

Navigation state not persisting

导航状态未持久化

Use proper navigation path management:
swift
@State private var path = NavigationPath()

// Restore path from storage
.task {
    if let savedPath = try? await loadPath() {
        path = savedPath
    }
}
使用正确的导航路径管理方式:
swift
@State private var path = NavigationPath()

// 从存储中恢复路径
.task {
    if let savedPath = try? await loadPath() {
        path = savedPath
    }
}

Performance issues with List

List性能问题

Use lazy loading and id-based updates:
swift
List(items, id: \.id) { item in
    ItemRow(item: item)
}
.id(items.map(\.id)) // Force refresh when needed
使用懒加载和基于ID的更新:
swift
List(items, id: \.id) { item in
    ItemRow(item: item)
}
.id(items.map(\.id)) // 需要时强制刷新

Related Skills

相关技能

Resources

资源