Install any skill in seconds. Free to start, no credit card required.
Get Started Free →@Observableを使用した状態管理、ビュー合成、ナビゲーション、パフォーマンス最適化、モダンなiOS/macOS UIのベストプラクティスを備えたSwiftUIアーキテクチャパターン。
.claude/skills/affaan-m-swiftui-patterns/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 38% | 0% |
| case-02 | ✓→✓ | = Same ✓ | 45% | 0% |
| case-03 | ✓→✓ | = Same ✓ | 49% | 0% |
| case-04 | ✓→✓ | = Same ✓ | 76% | 0% |
| case-05 | ✓→✓ | = Same ✓ | 144% | 0% |
适用于 Apple 平台的现代 SwiftUI 模式,用于构建声明式、高性能的用户界面。涵盖 Observation 框架、视图组合、类型安全导航和性能优化。
@State、@Observable、@Binding)NavigationStack 设计导航流程时选择最适合的最简单包装器:
| 包装器 | 使用场景 | |---------|----------| | @State | 视图本地的值类型(开关、表单字段、Sheet 展示) | | @Binding | 指向父视图 @State 的双向引用 | | @Observable 类 + @State | 拥有多个属性的自有模型 | | @Observable 类(无包装器) | 从父视图传递的只读引用 | | @Bindable | 指向 @Observable 属性的双向绑定 | | @Environment | 通过 .environment() 注入的共享依赖项 |
使用 @Observable(而非 ObservableObject)—— 它跟踪属性级别的变更,因此 SwiftUI 只会重新渲染读取了已变更属性的视图:
swift@Observable final class ItemListViewModel { private(set) var items: [Item] = [] private(set) var isLoading = false var searchText = "" private let repository: any ItemRepository init(repository: any ItemRepository = DefaultItemRepository()) { self.repository = repository } func load() async { isLoading = true defer { isLoading = false } items = (try? await repository.fetchAll()) ?? [] } }
swiftstruct ItemListView: View { @State private var viewModel: ItemListViewModel init(viewModel: ItemListViewModel = ItemListViewModel()) { _viewModel = State(initialValue: viewModel) } var body: some View { List(viewModel.items) { item in ItemRow(item: item) } .searchable(text: $viewModel.searchText) .overlay { if viewModel.isLoading { ProgressView() } } .task { await viewModel.load() } } }
用 @Environment 替换 @EnvironmentObject:
swift// Inject ContentView() .environment(authManager) // Consume struct ProfileView: View { @Environment(AuthManager.self) private var auth var body: some View { Text(auth.currentUser?.name ?? "Guest") } }
将视图拆分为小型、专注的结构体。当状态变更时,只有读取该状态的子视图会重新渲染:
swiftstruct OrderView: View { @State private var viewModel = OrderViewModel() var body: some View { VStack { OrderHeader(title: viewModel.title) OrderItemList(items: viewModel.items) OrderTotal(total: viewModel.total) } } }
swiftstruct CardModifier: ViewModifier { func body(content: Content) -> some View { content .padding() .background(.regularMaterial) .clipShape(RoundedRectangle(cornerRadius: 12)) } } extension View { func cardStyle() -> some View { modifier(CardModifier()) } }
使用 NavigationStack 与 NavigationPath 来实现程序化、类型安全的路由:
swift@Observable final class Router { var path = NavigationPath() func navigate(to destination: Destination) { path.append(destination) } func popToRoot() { path = NavigationPath() } } enum Destination: Hashable { case detail(Item.ID) case settings case profile(User.ID) } struct RootView: View { @State private var router = Router() var body: some View { NavigationStack(path: $router.path) { HomeView() .navigationDestination(for: Destination.self) { dest in switch dest { case .detail(let id): ItemDetailView(itemID: id) case .settings: SettingsView() case .profile(let id): ProfileView(userID: id) } } } .environment(router) } }
LazyVStack 和 LazyHStack 仅在视图可见时才创建它们:
swiftScrollView { LazyVStack(spacing: 8) { ForEach(items) { item in ItemRow(item: item) } } }
在 ForEach 中始终使用稳定、唯一的 ID —— 避免使用数组索引:
swift// Use Identifiable conformance or explicit id ForEach(items, id: \.stableID) { item in ItemRow(item: item) }
body 内执行 I/O、网络调用或繁重计算.task {} 处理异步工作 —— 当视图消失时它会自动取消.sensoryFeedback() 和 .geometryGroup().shadow()、.blur() 和 .mask() —— 它们会触发屏幕外渲染对于 body 计算昂贵的视图,遵循 Equatable 以跳过不必要的重新渲染:
swiftstruct ExpensiveChartView: View, Equatable { let dataPoints: [DataPoint] // DataPoint must conform to Equatable static func == (lhs: Self, rhs: Self) -> Bool { lhs.dataPoints == rhs.dataPoints } var body: some View { // Complex chart rendering } }
使用 #Preview 宏配合内联模拟数据以进行快速迭代:
swift#Preview("Empty state") { ItemListView(viewModel: ItemListViewModel(repository: EmptyMockRepository())) } #Preview("Loaded") { ItemListView(viewModel: ItemListViewModel(repository: PopulatedMockRepository())) }
ObservableObject / @Published / @StateObject / @EnvironmentObject —— 迁移到 @Observablebody 或 init 中 —— 使用 .task {} 或显式的加载方法@State —— 改为从父视图传递AnyView 类型擦除 —— 对于条件视图,优先选择 @ViewBuilder 或 GroupSendable 要求查看技能:swift-actor-persistence 以了解基于 Actor 的持久化模式。 查看技能:swift-protocol-di-testing 以了解基于协议的 DI 和使用 Swift Testing 进行测试。
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→pass | 14,774 | 19,850 | +34% | 1 | 1 | 0% | 2,868 | 3,966 | +38% | 0 | 0 | — |
case-02 | pass→pass | 14,565 | 16,969 | +17% | 1 | 1 | 0% | 2,784 | 4,028 | +45% | 0 | 0 | — |
case-03 | pass→pass | 12,447 | 8,170 | -34% | 1 | 1 | 0% | 2,175 | 3,241 | +49% | 0 | 0 | — |
case-04 | pass→pass | 10,256 | 6,645 | -35% | 1 | 1 | 0% | 1,642 | 2,882 | +76% | 0 | 0 | — |
case-05 | pass→pass | 6,327 | 7,015 | +11% | 1 | 1 | 0% | 1,030 | 2,517 | +144% | 0 | 0 | — |
case-14 | pass→pass | 8,214 | 8,032 | -2% | 1 | 1 | 0% | 1,389 | 3,067 | +121% | 0 | 0 | — |
case-06 | pass→pass | 5,462 | 4,381 | -20% | 1 | 1 | 0% | 825 | 2,468 | +199% | 0 | 0 | — |
case-07 | pass→pass | 4,348 | 2,917 | -33% | 1 | 1 | 0% | 701 | 2,226 | +218% | 0 | 0 | — |
case-08 | pass→pass | 19,261 | 15,076 | -22% | 1 | 1 | 0% | 3,226 | 4,378 | +36% | 0 | 0 | — |
case-09 | pass→pass | 10,935 | 7,213 | -34% | 1 | 1 | 0% | 2,009 | 3,101 | +54% | 0 | 0 | — |
case-10 | pass→pass | 9,060 | 6,474 | -29% | 1 | 1 | 0% | 1,563 | 2,756 | +76% | 0 | 0 | — |
case-11 | pass→pass | 10,903 | 8,230 | -25% | 1 | 1 | 0% | 1,964 | 3,145 | +60% | 0 | 0 | — |
case-12 | pass→pass | 9,894 | 7,317 | -26% | 1 | 1 | 0% | 1,710 | 3,142 | +84% | 0 | 0 | — |
case-13 | pass→pass | 16,656 | 10,384 | -38% | 1 | 1 | 0% | 2,782 | 3,480 | +25% | 0 | 0 | — |
case-15 | pass→pass | 11,291 | 11,884 | +5% | 1 | 1 | 0% | 2,071 | 3,942 | +90% | 0 | 0 | — |
case-16 | pass→pass | 20,917 | 8,672 | -59% | 1 | 1 | 0% | 2,019 | 3,151 | +56% | 0 | 0 | — |
case-17 | pass→pass | 13,446 | 11,447 | -15% | 1 | 1 | 0% | 2,156 | 3,445 | +60% | 0 | 0 | — |
case-18 | pass→pass | 11,563 | 7,376 | -36% | 1 | 1 | 0% | 2,075 | 3,030 | +46% | 0 | 0 | — |
case-19 | pass→pass | 10,086 | 10,036 | -0% | 1 | 1 | 0% | 1,753 | 3,318 | +89% | 0 | 0 | — |
case-20 | pass→pass | 20,948 | 16,291 | -22% | 1 | 1 | 0% | 3,388 | 4,724 | +39% | 0 | 0 | — |
case-21 | pass→pass | 16,179 | 15,081 | -7% | 1 | 1 | 0% | 2,940 | 4,515 | +54% | 0 | 0 | — |
case-22 | pass→pass | 11,940 | 9,531 | -20% | 1 | 1 | 0% | 2,344 | 3,627 | +55% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +5 percentage points is the difference between those two pass rates over the 22 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.