diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 712ed59..8115333 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -7,12 +7,12 @@ "email": "contact@avanderlee.com" }, "metadata": { - "description": "SwiftUI best practices for state management, modern APIs, view composition, performance, and iOS 26+ Liquid Glass." + "description": "SwiftUI best practices for state management, view composition, performance, and iOS 26+ Liquid Glass." }, "plugins": [ { "name": "swiftui-expert", - "description": "Expert SwiftUI guidance for modern APIs, state management, performance, and Liquid Glass patterns.", + "description": "Expert SwiftUI guidance for state management, view composition, performance, and Liquid Glass patterns.", "repository": "https://github.com/avanderlee/SwiftUI-Agent-Skill", "version": "1.1.3", "author": { @@ -27,7 +27,6 @@ "ios", "apple", "state-management", - "modern-apis", "performance", "liquid-glass" ], @@ -37,7 +36,6 @@ "ios", "apple", "state-management", - "modern-apis", "performance", "liquid-glass" ], diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index b66698c..f1c4b90 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "swiftui-expert", "version": "1.1.3", - "description": "Expert SwiftUI guidance for state management, view composition, performance, modern APIs, and iOS 26+ Liquid Glass adoption.", + "description": "Expert SwiftUI guidance for state management, view composition, performance, and iOS 26+ Liquid Glass adoption.", "author": { "name": "Antoine van der Lee", "email": "contact@avanderlee.com" @@ -14,7 +14,6 @@ "ios", "apple", "state-management", - "modern-apis", "performance", "liquid-glass", "view-composition", diff --git a/AGENTS.md b/AGENTS.md index 7170967..29b0048 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -61,27 +61,10 @@ This document provides guidance for AI agents working with this skill to ensure **Do not automatically apply optimizations.** Let developers decide based on their performance needs. -### Modern API Usage - -**Enforce modern API usage for correctness:** -- ✅ `foregroundStyle()` instead of `foregroundColor()` -- ✅ `NavigationStack` instead of `NavigationView` -- ✅ `@Observable` instead of `ObservableObject` for new code - -These are about using current, non-deprecated APIs, not optimization. - -### State Management - -**Be clear about `@MainActor` requirements:** -- Mention that `@Observable` classes may need `@MainActor` -- Note that projects with default actor isolation don't need explicit `@MainActor` -- Don't mandate it as "always required" - ## What to Include ### ✅ Include These Topics: - Property wrapper selection (`@State`, `@Binding`, `@Observable`, etc.) -- Modern API replacements for deprecated APIs - View composition and extraction patterns - Performance patterns (stable identity, lazy loading, etc.) - Common pitfalls and how to avoid them @@ -102,7 +85,6 @@ These are about using current, non-deprecated APIs, not optimization. ## Language and Tone ### Use Clear, Direct Language: -- "Use X instead of Y" (for deprecated APIs) - "Consider X when Y" (for optimizations) - "Avoid X because Y" (for anti-patterns) - "X is preferred over Y" (for best practices) diff --git a/README.md b/README.md index 7b8724b..606e291 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # SwiftUI Expert Skill -Expert guidance for any AI coding tool that supports the [Agent Skills open format](https://agentskills.io/home) — modern SwiftUI APIs, state management, performance, and iOS 26+ Liquid Glass adoption. +Expert guidance for any AI coding tool that supports the [Agent Skills open format](https://agentskills.io/home) — SwiftUI state management, view composition, performance, and iOS 26+ Liquid Glass adoption. This repository distills practical SwiftUI best practices into actionable, concise references for agents and code review workflows. @@ -26,7 +26,7 @@ npx skills add https://github.com/avdlee/swiftui-agent-skill --skill swiftui-exp For more information, [visit the skills.sh platform page](https://skills.sh/avdlee/swiftui-agent-skill/swiftui-expert-skill). Then use the skill in your AI agent, for example: -> Use the swiftui expert skill and review the current SwiftUI code for state-management and modern API improvements +> Use the swiftui expert skill and review the current SwiftUI code for state-management and performance improvements ### Option B: Claude Code Plugin @@ -87,7 +87,6 @@ This skill gives your AI coding tool practical SwiftUI guidance. It can: ### Guide Your SwiftUI Decisions - Choose the right state management tool (`@State`, `@Binding`, `@Observable`, `@Bindable`) -- Recommend modern replacements for deprecated SwiftUI APIs - Provide clear guidance for sheets, navigation, scrolling, and lists - Advise on iOS 26+ Liquid Glass usage with safe availability fallbacks @@ -103,9 +102,7 @@ This skill gives your AI coding tool practical SwiftUI guidance. It can: ## What Makes This Skill Different -**Non-Opinionated**: Focuses on SwiftUI correctness and modern APIs, not forcing an architecture, project structure, or code style. - -**Modern-first**: Calls out deprecated APIs and offers up-to-date replacements. +**Non-Opinionated**: Focuses on SwiftUI correctness, not forcing an architecture, project structure, or code style. **Practical & concise**: Treats the agent as capable; provides the checklists and pitfalls that actually matter in day-to-day SwiftUI work. @@ -122,12 +119,10 @@ swiftui-expert-skill/ layout-best-practices.md - Layout patterns and GeometryReader alternatives liquid-glass.md - iOS 26+ glass effects and fallback patterns list-patterns.md - ForEach identity and list performance - modern-apis.md - Deprecated API replacements performance-patterns.md - Hot-path optimizations and update control scroll-patterns.md - ScrollViewReader and programmatic scrolling sheet-navigation-patterns.md - Sheets and type-safe navigation state-management.md - Property wrapper selection and data flow - text-formatting.md - Modern Text formatting and string utilities view-structure.md - View extraction and composition patterns ``` @@ -145,8 +140,6 @@ Please read [CONTRIBUTING.md](CONTRIBUTING.md) for: Several SwiftUI guidelines in this skill were inspired by or derived from the following works: -- [SwiftAgents](https://github.com/twostraws/SwiftAgents) by [Paul Hudson](https://www.hackingwithswift.com) — an AGENTS.md for Swift and SwiftUI projects based on his article [What to fix in AI-generated Swift code](https://www.hackingwithswift.com/articles/281/what-to-fix-in-ai-generated-swift-code). His curation of modern SwiftUI best practices for AI agents has been the foundation for this skill. -- AGENTS file input from the developers of [Helm for App Store Connect](https://helm-app.com) - [Skills](https://github.com/Dimillian/Skills) by [Thomas Ricouard](https://github.com/Dimillian) — a collection of SwiftUI-focused Codex skills covering UI patterns, performance auditing, and Liquid Glass. ## About the authors diff --git a/swiftui-expert-skill/SKILL.md b/swiftui-expert-skill/SKILL.md index ce1a826..0bc9d9f 100644 --- a/swiftui-expert-skill/SKILL.md +++ b/swiftui-expert-skill/SKILL.md @@ -1,18 +1,17 @@ --- name: swiftui-expert-skill -description: Write, review, or improve SwiftUI code following best practices for state management, view composition, performance, modern APIs, Swift concurrency, and iOS 26+ Liquid Glass adoption. Use when building new SwiftUI features, refactoring existing views, reviewing code quality, or adopting modern SwiftUI patterns. +description: Write, review, or improve SwiftUI code following best practices for state management, view composition, performance, and iOS 26+ Liquid Glass adoption. Use when building new SwiftUI features, refactoring existing views, reviewing code quality, or adopting modern SwiftUI patterns. --- # SwiftUI Expert Skill ## Overview -Use this skill to build, review, or improve SwiftUI features with correct state management, modern API usage, Swift concurrency best practices, optimal view composition, and iOS 26+ Liquid Glass styling. Prioritize native APIs, Apple design guidance, and performance-conscious patterns. This skill focuses on facts and best practices without enforcing specific architectural patterns. +Use this skill to build, review, or improve SwiftUI features with correct state management, optimal view composition, and iOS 26+ Liquid Glass styling. Prioritize native APIs, Apple design guidance, and performance-conscious patterns. This skill focuses on facts and best practices without enforcing specific architectural patterns. ## Workflow Decision Tree ### 1) Review existing SwiftUI code - Check property wrapper usage against the selection guide (see `references/state-management.md`) -- Verify modern API usage (see `references/modern-apis.md`) - Verify view composition follows extraction rules (see `references/view-structure.md`) - Check performance patterns are applied (see `references/performance-patterns.md`) - Verify list patterns use stable identity (see `references/list-patterns.md`) @@ -21,8 +20,7 @@ Use this skill to build, review, or improve SwiftUI features with correct state - Validate iOS 26+ availability handling with sensible fallbacks ### 2) Improve existing SwiftUI code -- Audit state management for correct wrapper selection (prefer `@Observable` over `ObservableObject`) -- Replace deprecated APIs with modern equivalents (see `references/modern-apis.md`) +- Audit state management for correct wrapper selection (see `references/state-management.md`) - Extract complex views into separate subviews (see `references/view-structure.md`) - Refactor hot paths to minimize redundant state updates (see `references/performance-patterns.md`) - Ensure ForEach uses stable identity (see `references/list-patterns.md`) @@ -32,10 +30,8 @@ Use this skill to build, review, or improve SwiftUI features with correct state ### 3) Implement new SwiftUI feature - Design data flow first: identify owned vs injected state (see `references/state-management.md`) -- Use modern APIs (no deprecated modifiers or patterns, see `references/modern-apis.md`) -- Use `@Observable` for shared state (with `@MainActor` if not using default actor isolation) -- Structure views for optimal diffing (extract subviews early, keep views small, see `references/view-structure.md`) -- Separate business logic into testable models (see `references/layout-best-practices.md`) +- Structure views for optimal diffing (extract subviews early, see `references/view-structure.md`) +- Keep business logic in services and models for testability (see `references/layout-best-practices.md`) - Use correct animation patterns (implicit vs explicit, transitions, see `references/animation-basics.md`, `references/animation-transitions.md`, `references/animation-advanced.md`) - Apply glass effects after layout/appearance modifiers (see `references/liquid-glass.md`) - Gate iOS 26+ features with `#available` and provide fallbacks @@ -43,49 +39,22 @@ Use this skill to build, review, or improve SwiftUI features with correct state ## Core Guidelines ### State Management -- **Always prefer `@Observable` over `ObservableObject`** for new code -- **Mark `@Observable` classes with `@MainActor`** unless using default actor isolation -- **Always mark `@State` and `@StateObject` as `private`** (makes dependencies clear) -- **Never declare passed values as `@State` or `@StateObject`** (they only accept initial values) -- Use `@State` with `@Observable` classes (not `@StateObject`) -- `@Binding` only when child needs to **modify** parent state -- `@Bindable` for injected `@Observable` objects needing bindings +- `@State` must be `private`; use for internal view state +- `@Binding` only when a child needs to **modify** parent state +- `@StateObject` when view **creates** the object; `@ObservedObject` when **injected** +- iOS 17+: Use `@State` with `@Observable` classes; use `@Bindable` for injected observables needing bindings - Use `let` for read-only values; `var` + `.onChange()` for reactive reads -- Legacy: `@StateObject` for owned `ObservableObject`; `@ObservedObject` for injected -- Nested `ObservableObject` doesn't work (pass nested objects directly); `@Observable` handles nesting fine - -### Modern APIs -- Use `foregroundStyle()` instead of `foregroundColor()` -- Use `clipShape(.rect(cornerRadius:))` instead of `cornerRadius()` -- Use `Tab` API instead of `tabItem()` -- Use `Button` instead of `onTapGesture()` (unless need location/count) -- Use `NavigationStack` instead of `NavigationView` -- Use `navigationDestination(for:)` for type-safe navigation -- Use two-parameter or no-parameter `onChange()` variant -- Use `ImageRenderer` for rendering SwiftUI views -- Use `.sheet(item:)` instead of `.sheet(isPresented:)` for model-based content -- Sheets should own their actions and call `dismiss()` internally -- Use `ScrollViewReader` for programmatic scrolling with stable IDs -- Avoid `UIScreen.main.bounds` for sizing -- Avoid `GeometryReader` when alternatives exist (e.g., `containerRelativeFrame()`) - -### Swift Best Practices -- Use modern Text formatting (`.format` parameters, not `String(format:)`) -- Use `localizedStandardContains()` for user-input filtering (not `contains()`) -- Prefer static member lookup (`.blue` vs `Color.blue`) -- Use `.task` modifier for automatic cancellation of async work -- Use `.task(id:)` for value-dependent tasks +- Never pass values into `@State` or `@StateObject` — they only accept initial values +- Nested `ObservableObject` doesn't propagate changes — pass nested objects directly; `@Observable` handles nesting fine ### View Composition -- **Prefer modifiers over conditional views** for state changes (maintains view identity) - Extract complex views into separate subviews for better readability and performance -- Keep views small for optimal performance +- Prefer modifiers over conditional views for state changes (maintains view identity) - Keep view `body` simple and pure (no side effects or complex logic) - Use `@ViewBuilder` functions only for small, simple sections - Prefer `@ViewBuilder let content: Content` over closure-based content properties -- Separate business logic into testable models (not about enforcing architectures) +- Keep business logic in services and models; views should orchestrate UI flow - Action handlers should reference methods, not contain inline logic -- Use relative layout over hard-coded constants - Views should work in any context (don't assume screen size or presentation style) ### Performance @@ -126,36 +95,17 @@ Use this skill to build, review, or improve SwiftUI features with correct state ## Quick Reference -### Property Wrapper Selection (Modern) +### Property Wrapper Selection | Wrapper | Use When | |---------|----------| -| `@State` | Internal view state (must be `private`), or owned `@Observable` class | +| `@State` | Internal view state (must be `private`) | | `@Binding` | Child modifies parent's state | -| `@Bindable` | Injected `@Observable` needing bindings | +| `@StateObject` | View owns an `ObservableObject` | +| `@ObservedObject` | View receives an `ObservableObject` | +| `@Bindable` | iOS 17+: Injected `@Observable` needing bindings | | `let` | Read-only value from parent | | `var` | Read-only value watched via `.onChange()` | -**Legacy (Pre-iOS 17):** -| Wrapper | Use When | -|---------|----------| -| `@StateObject` | View owns an `ObservableObject` (use `@State` with `@Observable` instead) | -| `@ObservedObject` | View receives an `ObservableObject` | - -### Modern API Replacements -| Deprecated | Modern Alternative | -|------------|-------------------| -| `foregroundColor()` | `foregroundStyle()` | -| `cornerRadius()` | `clipShape(.rect(cornerRadius:))` | -| `tabItem()` | `Tab` API | -| `onTapGesture()` | `Button` (unless need location/count) | -| `NavigationView` | `NavigationStack` | -| `onChange(of:) { value in }` | `onChange(of:) { old, new in }` or `onChange(of:) { }` | -| `fontWeight(.bold)` | `bold()` | -| `GeometryReader` | `containerRelativeFrame()` or `visualEffect()` | -| `showsIndicators: false` | `.scrollIndicators(.hidden)` | -| `String(format: "%.2f", value)` | `Text(value, format: .number.precision(.fractionLength(2)))` | -| `string.contains(search)` | `string.localizedStandardContains(search)` (for user input) | - ### Liquid Glass Patterns ```swift // Basic glass effect with fallback @@ -185,42 +135,23 @@ Button("Confirm") { } ## Review Checklist ### State Management -- [ ] Using `@Observable` instead of `ObservableObject` for new code -- [ ] `@Observable` classes marked with `@MainActor` (if needed) -- [ ] Using `@State` with `@Observable` classes (not `@StateObject`) -- [ ] `@State` and `@StateObject` properties are `private` -- [ ] Passed values NOT declared as `@State` or `@StateObject` +- [ ] `@State` properties are `private` - [ ] `@Binding` only where child modifies parent state -- [ ] `@Bindable` for injected `@Observable` needing bindings +- [ ] `@StateObject` for owned, `@ObservedObject` for injected +- [ ] iOS 17+: `@State` with `@Observable`, `@Bindable` for injected +- [ ] Passed values NOT declared as `@State` or `@StateObject` - [ ] Nested `ObservableObject` avoided (or passed directly to child views) -### Modern APIs (see `references/modern-apis.md`) -- [ ] Using `foregroundStyle()` instead of `foregroundColor()` -- [ ] Using `clipShape(.rect(cornerRadius:))` instead of `cornerRadius()` -- [ ] Using `Tab` API instead of `tabItem()` -- [ ] Using `Button` instead of `onTapGesture()` (unless need location/count) -- [ ] Using `NavigationStack` instead of `NavigationView` -- [ ] Avoiding `UIScreen.main.bounds` -- [ ] Using alternatives to `GeometryReader` when possible -- [ ] Button images include text labels for accessibility - ### Sheets & Navigation (see `references/sheet-navigation-patterns.md`) - [ ] Using `.sheet(item:)` for model-based sheets - [ ] Sheets own their actions and dismiss internally -- [ ] Using `navigationDestination(for:)` for type-safe navigation ### ScrollView (see `references/scroll-patterns.md`) - [ ] Using `ScrollViewReader` with stable IDs for programmatic scrolling -- [ ] Using `.scrollIndicators(.hidden)` instead of initializer parameter - -### Text & Formatting (see `references/text-formatting.md`) -- [ ] Using modern Text formatting (not `String(format:)`) -- [ ] Using `localizedStandardContains()` for search filtering ### View Structure (see `references/view-structure.md`) - [ ] Using modifiers instead of conditionals for state changes - [ ] Complex views extracted to separate subviews -- [ ] Views kept small for performance - [ ] Container views use `@ViewBuilder let content: Content` ### Performance (see `references/performance-patterns.md`) @@ -241,7 +172,7 @@ Button("Confirm") { } ### Layout (see `references/layout-best-practices.md`) - [ ] Avoiding layout thrash (deep hierarchies, excessive GeometryReader) - [ ] Gating frequent geometry updates by thresholds -- [ ] Business logic separated into testable models +- [ ] Business logic kept in services and models (not in views) - [ ] Action handlers reference methods (not inline logic) - [ ] Using relative layout (not hard-coded constants) - [ ] Views work in any context (context-agnostic) @@ -264,18 +195,16 @@ Button("Confirm") { } - [ ] Shapes and tints consistent across related elements ## References -- `references/state-management.md` - Property wrappers and data flow (prefer `@Observable`) +- `references/state-management.md` - Property wrappers and data flow - `references/view-structure.md` - View composition, extraction, and container patterns - `references/performance-patterns.md` - Performance optimization techniques and anti-patterns - `references/list-patterns.md` - ForEach identity, stability, and list best practices - `references/layout-best-practices.md` - Layout patterns, context-agnostic views, and testability -- `references/modern-apis.md` - Modern API usage and deprecated replacements - `references/animation-basics.md` - Core animation concepts, implicit/explicit animations, timing, performance - `references/animation-transitions.md` - Transitions, custom transitions, Animatable protocol - `references/animation-advanced.md` - Transactions, phase/keyframe animations (iOS 17+), completion handlers (iOS 17+) - `references/sheet-navigation-patterns.md` - Sheet presentation and navigation patterns - `references/scroll-patterns.md` - ScrollView patterns and programmatic scrolling -- `references/text-formatting.md` - Modern text formatting and string operations - `references/image-optimization.md` - AsyncImage, image downsampling, and optimization - `references/liquid-glass.md` - iOS 26+ Liquid Glass API @@ -284,7 +213,5 @@ Button("Confirm") { } This skill focuses on **facts and best practices**, not architectural opinions: - We don't enforce specific architectures (e.g., MVVM, VIPER) - We do encourage separating business logic for testability -- We prioritize modern APIs over deprecated ones -- We emphasize thread safety with `@MainActor` and `@Observable` - We optimize for performance and maintainability - We follow Apple's Human Interface Guidelines and API design patterns diff --git a/swiftui-expert-skill/references/image-optimization.md b/swiftui-expert-skill/references/image-optimization.md index 4511776..d3eea6c 100644 --- a/swiftui-expert-skill/references/image-optimization.md +++ b/swiftui-expert-skill/references/image-optimization.md @@ -242,36 +242,6 @@ Image(systemName: "star.square.fill") Image(systemName: "folder.badge.plus") ``` -## Image Rendering - -### ImageRenderer for Snapshots - -```swift -// Render SwiftUI view to UIImage -let renderer = ImageRenderer(content: myView) -renderer.scale = UIScreen.main.scale - -if let uiImage = renderer.uiImage { - // Use the image (save, share, etc.) -} - -// Render to CGImage -if let cgImage = renderer.cgImage { - // Use CGImage -} -``` - -### Rendering with Custom Size - -```swift -let renderer = ImageRenderer(content: myView) -renderer.proposedSize = ProposedViewSize(width: 400, height: 300) - -if let uiImage = renderer.uiImage { - // Image rendered at 400x300 points -} -``` - ## Summary Checklist - [ ] Use `AsyncImage` with proper phase handling @@ -281,6 +251,5 @@ if let uiImage = renderer.uiImage { - [ ] Use appropriate target sizes for downsampling - [ ] Consider image caching for frequently accessed images - [ ] Use SF Symbols with appropriate rendering modes -- [ ] Use `ImageRenderer` for rendering SwiftUI views to images **Performance Note**: Image downsampling is an optional optimization. Only suggest it when you encounter `UIImage(data:)` usage in performance-sensitive contexts like scrollable lists or grids. diff --git a/swiftui-expert-skill/references/layout-best-practices.md b/swiftui-expert-skill/references/layout-best-practices.md index 57c8f74..ae83c90 100644 --- a/swiftui-expert-skill/references/layout-best-practices.md +++ b/swiftui-expert-skill/references/layout-best-practices.md @@ -164,17 +164,15 @@ containerRelativeFrame(.horizontal) { width, _ in ## View Logic and Testability -### Separate View Logic from Views +### Keep Business Logic in Services and Models -**Place view logic into view models or similar, so it can be tested.** +**Business logic belongs in services and models, not in views.** Views should stay simple and declarative — orchestrating UI state, not implementing business rules. This makes logic independently testable without requiring view instantiation. -> **iOS 17+**: Use `@Observable` macro with `@State` for view models. +> **iOS 17+**: Use `@Observable` with `@State`. ```swift -// Good - logic in testable model (iOS 17+) @Observable -@MainActor -final class LoginViewModel { +final class AuthService { var email = "" var password = "" var isValid: Bool { @@ -182,34 +180,32 @@ final class LoginViewModel { } func login() async throws { - // Business logic here + // Business logic here — testable without the view } } struct LoginView: View { - @State private var viewModel = LoginViewModel() + @State private var authService = AuthService() var body: some View { Form { - TextField("Email", text: $viewModel.email) - SecureField("Password", text: $viewModel.password) + TextField("Email", text: $authService.email) + SecureField("Password", text: $authService.password) Button("Login") { Task { - try? await viewModel.login() + try? await authService.login() } } - .disabled(!viewModel.isValid) + .disabled(!authService.isValid) } } } ``` -> **iOS 16 and earlier**: Use `ObservableObject` protocol with `@StateObject`. +> **iOS 16 and earlier**: Use `ObservableObject` with `@StateObject`. ```swift -// Good - logic in testable model (iOS 16 and earlier) -@MainActor -final class LoginViewModel: ObservableObject { +final class AuthService: ObservableObject { @Published var email = "" @Published var password = "" var isValid: Bool { @@ -217,30 +213,30 @@ final class LoginViewModel: ObservableObject { } func login() async throws { - // Business logic here + // Business logic here — testable without the view } } struct LoginView: View { - @StateObject private var viewModel = LoginViewModel() + @StateObject private var authService = AuthService() var body: some View { Form { - TextField("Email", text: $viewModel.email) - SecureField("Password", text: $viewModel.password) + TextField("Email", text: $authService.email) + SecureField("Password", text: $authService.password) Button("Login") { Task { - try? await viewModel.login() + try? await authService.login() } } - .disabled(!viewModel.isValid) + .disabled(!authService.isValid) } } } ``` ```swift -// Bad - logic embedded in view +// Bad - logic embedded in view (not testable) struct LoginView: View { @State private var email = "" @State private var password = "" @@ -250,7 +246,6 @@ struct LoginView: View { TextField("Email", text: $email) SecureField("Password", text: $password) Button("Login") { - // Business logic directly in view - hard to test Task { if !email.isEmpty && password.count >= 8 { // Login logic... @@ -262,7 +257,7 @@ struct LoginView: View { } ``` -**Note**: This is about separating business logic for testability, not about enforcing specific architectures like MVVM. The goal is to make logic testable while keeping views simple. +**Note**: This is about making business logic testable, not about enforcing a specific architecture. Whether you call them services, models, or something else — the key is that logic lives outside views where it can be tested independently. ## Action Handlers @@ -271,10 +266,10 @@ struct LoginView: View { ```swift // Good - action references method struct PublishView: View { - @State private var viewModel = PublishViewModel() + @State private var publishService = PublishService() var body: some View { - Button("Publish Project", action: viewModel.handlePublish) + Button("Publish Project", action: publishService.handlePublish) } } @@ -306,7 +301,7 @@ struct PublishView: View { - [ ] Custom views own static containers - [ ] Avoid deep view hierarchies (layout thrash) - [ ] Gate frequent geometry updates by thresholds -- [ ] View logic separated into testable models/classes +- [ ] Business logic kept in services and models (not in views) - [ ] Action handlers reference methods, not inline logic - [ ] Avoid excessive `GeometryReader` usage - [ ] Use `containerRelativeFrame()` when appropriate diff --git a/swiftui-expert-skill/references/modern-apis.md b/swiftui-expert-skill/references/modern-apis.md deleted file mode 100644 index 20a5457..0000000 --- a/swiftui-expert-skill/references/modern-apis.md +++ /dev/null @@ -1,400 +0,0 @@ -# Modern SwiftUI APIs Reference - -## Overview - -This reference covers modern SwiftUI API usage patterns and deprecated API replacements. Always use the latest APIs to ensure forward compatibility and access to new features. - -## Styling and Appearance - -### foregroundStyle() vs foregroundColor() - -**Always use `foregroundStyle()` instead of `foregroundColor()`.** - -```swift -// Modern (Correct) -Text("Hello") - .foregroundStyle(.primary) - -Image(systemName: "star") - .foregroundStyle(.blue) - -// Legacy (Avoid) -Text("Hello") - .foregroundColor(.primary) -``` - -**Why**: `foregroundStyle()` supports hierarchical styles, gradients, and materials, making it more flexible and future-proof. - -### clipShape() vs cornerRadius() - -**Always use `clipShape(.rect(cornerRadius:))` instead of `cornerRadius()`.** - -```swift -// Modern (Correct) -Image("photo") - .clipShape(.rect(cornerRadius: 12)) - -VStack { - // content -} -.clipShape(.rect(cornerRadius: 16)) - -// Legacy (Avoid) -Image("photo") - .cornerRadius(12) -``` - -**Why**: `cornerRadius()` is deprecated. `clipShape()` is more explicit and supports all shape types. - -### fontWeight() vs bold() - -**Don't apply `fontWeight()` unless there's a good reason. Always use `bold()` for bold text.** - -```swift -// Correct -Text("Important") - .bold() - -// Avoid (unless you need a specific weight) -Text("Important") - .fontWeight(.bold) - -// Acceptable (specific weight needed) -Text("Semibold") - .fontWeight(.semibold) -``` - -## Navigation - -### NavigationStack vs NavigationView - -**Always use `NavigationStack` instead of `NavigationView`.** - -```swift -// Modern (Correct) -NavigationStack { - List(items) { item in - NavigationLink(value: item) { - Text(item.name) - } - } - .navigationDestination(for: Item.self) { item in - DetailView(item: item) - } -} - -// Legacy (Avoid) -NavigationView { - List(items) { item in - NavigationLink(destination: DetailView(item: item)) { - Text(item.name) - } - } -} -``` - -### navigationDestination(for:) - -**Use `navigationDestination(for:)` for type-safe navigation.** - -```swift -struct ContentView: View { - var body: some View { - NavigationStack { - List { - NavigationLink("Profile", value: Route.profile) - NavigationLink("Settings", value: Route.settings) - } - .navigationDestination(for: Route.self) { route in - switch route { - case .profile: - ProfileView() - case .settings: - SettingsView() - } - } - } - } -} - -enum Route: Hashable { - case profile - case settings -} -``` - -## Tabs - -### Tab API vs tabItem() - -**For iOS 18 and later, prefer the `Tab` API over `tabItem()` to access modern tab features, and use availability checks or `tabItem()` for earlier OS versions.** - -```swift -// Modern (Correct) - iOS 18+ -TabView { - Tab("Home", systemImage: "house") { - HomeView() - } - - Tab("Search", systemImage: "magnifyingglass") { - SearchView() - } - - Tab("Profile", systemImage: "person") { - ProfileView() - } -} - -// Legacy (Avoid) -TabView { - HomeView() - .tabItem { - Label("Home", systemImage: "house") - } -} -``` - -**Important**: When using `Tab(role:)` with roles, you must use the new `Tab { } label: { }` syntax for all tabs. Mixing with `.tabItem()` causes compilation errors. - -```swift -// Correct - all tabs use Tab syntax -TabView { - Tab(role: .search) { - SearchView() - } label: { - Label("Search", systemImage: "magnifyingglass") - } - - Tab { - HomeView() - } label: { - Label("Home", systemImage: "house") - } -} - -// Wrong - mixing Tab and tabItem causes errors -TabView { - Tab(role: .search) { - SearchView() - } label: { - Label("Search", systemImage: "magnifyingglass") - } - - HomeView() // Error: can't mix with Tab(role:) - .tabItem { - Label("Home", systemImage: "house") - } -} -``` - -## Interactions - -### Button vs onTapGesture() - -**Never use `onTapGesture()` unless you specifically need tap location or tap count. Always use `Button` otherwise.** - -```swift -// Correct - standard tap action -Button("Tap me") { - performAction() -} - -// Correct - need tap location -Text("Tap anywhere") - .onTapGesture { location in - handleTap(at: location) - } - -// Correct - need tap count -Image("photo") - .onTapGesture(count: 2) { - handleDoubleTap() - } - -// Wrong - use Button instead -Text("Tap me") - .onTapGesture { - performAction() - } -``` - -**Why**: `Button` provides proper accessibility, visual feedback, and semantic meaning. Use `onTapGesture()` only when you need its specific features. - -### Button with Images - -**Always specify text alongside images in buttons for accessibility.** - -```swift -// Correct - includes text label -Button("Add Item", systemImage: "plus") { - addItem() -} - -// Also correct - custom label -Button { - addItem() -} label: { - Label("Add Item", systemImage: "plus") -} - -// Wrong - image only, no text -Button { - addItem() -} label: { - Image(systemName: "plus") -} -``` - -## Layout and Sizing - -### Avoid UIScreen.main.bounds - -**Never use `UIScreen.main.bounds` to read available space.** - -```swift -// Wrong - uses UIKit, doesn't respect safe areas -let screenWidth = UIScreen.main.bounds.width - -// Correct - use GeometryReader -GeometryReader { geometry in - Text("Width: \(geometry.size.width)") -} - -// Better - use containerRelativeFrame (iOS 17+) -Text("Full width") - .containerRelativeFrame(.horizontal) - -// Best - let SwiftUI handle sizing -Text("Auto-sized") - .frame(maxWidth: .infinity) -``` - -### GeometryReader Alternatives - -> **iOS 17+**: `containerRelativeFrame` and `visualEffect` require iOS 17 or later. - -**Don't use `GeometryReader` if a newer alternative works.** - -```swift -// Modern - containerRelativeFrame -Image("hero") - .resizable() - .containerRelativeFrame(.horizontal) { length, axis in - length * 0.8 - } - -// Modern - visualEffect for position-based effects -Text("Parallax") - .visualEffect { content, geometry in - content.offset(y: geometry.frame(in: .global).minY * 0.5) - } - -// Legacy - only use if necessary -GeometryReader { geometry in - Image("hero") - .frame(width: geometry.size.width * 0.8) -} -``` - -## Type Erasure - -### Avoid AnyView - -**Avoid `AnyView` unless absolutely required.** - -```swift -// Prefer - use @ViewBuilder -@ViewBuilder -func content() -> some View { - if condition { - Text("Option A") - } else { - Image(systemName: "photo") - } -} - -// Avoid - type erasure has performance cost -func content() -> AnyView { - if condition { - return AnyView(Text("Option A")) - } else { - return AnyView(Image(systemName: "photo")) - } -} - -// Acceptable - when protocol conformance requires it -var body: some View { - // Complex conditional logic that requires type erasure -} -``` - -## Styling Best Practices - -### Dynamic Type - -**Don't force specific font sizes. Prefer Dynamic Type.** - -```swift -// Correct - respects user's text size preferences -Text("Title") - .font(.title) - -Text("Body") - .font(.body) - -// Avoid - fixed size doesn't scale -Text("Title") - .font(.system(size: 24)) -``` - -### UIKit Colors - -**Avoid using UIKit colors in SwiftUI code.** - -```swift -// Correct - SwiftUI colors -Text("Hello") - .foregroundStyle(.blue) - .background(.gray.opacity(0.2)) - -// Wrong - UIKit colors -Text("Hello") - .foregroundColor(Color(UIColor.systemBlue)) - .background(Color(UIColor.systemGray)) -``` - -## Static Member Lookup - -**Prefer static member lookup to struct instances.** - -```swift -// Correct - static member lookup -Circle() - .fill(.blue) -Button("Action") { } - .buttonStyle(.borderedProminent) - -// Verbose - unnecessary struct instantiation -Circle() - .fill(Color.blue) -Button("Action") { } - .buttonStyle(BorderedProminentButtonStyle()) -``` - -## Summary Checklist - -- [ ] Use `foregroundStyle()` instead of `foregroundColor()` -- [ ] Use `clipShape(.rect(cornerRadius:))` instead of `cornerRadius()` -- [ ] Use `Tab` API instead of `tabItem()` -- [ ] Use `Button` instead of `onTapGesture()` (unless need location/count) -- [ ] Use `NavigationStack` instead of `NavigationView` -- [ ] Use `navigationDestination(for:)` for type-safe navigation -- [ ] Avoid `AnyView` unless required -- [ ] Avoid `UIScreen.main.bounds` -- [ ] Avoid `GeometryReader` when alternatives exist -- [ ] Use Dynamic Type instead of fixed font sizes -- [ ] Avoid hard-coded padding/spacing unless requested -- [ ] Avoid UIKit colors in SwiftUI -- [ ] Use static member lookup (`.blue` vs `Color.blue`) -- [ ] Include text labels with button images -- [ ] Use `bold()` instead of `fontWeight(.bold)` diff --git a/swiftui-expert-skill/references/scroll-patterns.md b/swiftui-expert-skill/references/scroll-patterns.md index a234267..2c4b987 100644 --- a/swiftui-expert-skill/references/scroll-patterns.md +++ b/swiftui-expert-skill/references/scroll-patterns.md @@ -1,24 +1,5 @@ # SwiftUI ScrollView Patterns Reference -## ScrollView Modifiers - -### Hiding Scroll Indicators - -**Use `.scrollIndicators(.hidden)` modifier instead of initializer parameter.** - -```swift -// Modern (Correct) -ScrollView { - content -} -.scrollIndicators(.hidden) - -// Legacy (Avoid) -ScrollView(showsIndicators: false) { - content -} -``` - ## ScrollViewReader for Programmatic Scrolling **Use `ScrollViewReader` for scroll-to-top, scroll-to-bottom, and anchor-based jumps.** @@ -295,7 +276,6 @@ struct SnapScrollView: View { ## Summary Checklist -- [ ] Use `.scrollIndicators(.hidden)` instead of initializer parameter - [ ] Use `ScrollViewReader` with stable IDs for programmatic scrolling - [ ] Always use explicit animations with `scrollTo()` - [ ] Use `.visualEffect` for scroll-based visual changes diff --git a/swiftui-expert-skill/references/text-formatting.md b/swiftui-expert-skill/references/text-formatting.md deleted file mode 100644 index 9bce545..0000000 --- a/swiftui-expert-skill/references/text-formatting.md +++ /dev/null @@ -1,285 +0,0 @@ -# SwiftUI Text Formatting Reference - -## Modern Text Formatting - -**Never use C-style `String(format:)` with Text. Always use format parameters.** - -## Number Formatting - -### Basic Number Formatting - -```swift -let value = 42.12345 - -// Modern (Correct) -Text(value, format: .number.precision(.fractionLength(2))) -// Output: "42.12" - -Text(abs(value), format: .number.precision(.fractionLength(2))) -// Output: "42.12" (absolute value) - -// Legacy (Avoid) -Text(String(format: "%.2f", abs(value))) -``` - -### Integer Formatting - -```swift -let count = 1234567 - -// With grouping separator -Text(count, format: .number) -// Output: "1,234,567" (locale-dependent) - -// Without grouping -Text(count, format: .number.grouping(.never)) -// Output: "1234567" -``` - -### Decimal Precision - -```swift -let price = 19.99 - -// Fixed decimal places -Text(price, format: .number.precision(.fractionLength(2))) -// Output: "19.99" - -// Significant digits -Text(price, format: .number.precision(.significantDigits(3))) -// Output: "20.0" - -// Integer-only -Text(price, format: .number.precision(.integerLength(1...))) -// Output: "19" -``` - -## Currency Formatting - -```swift -let price = 19.99 - -// Correct - with currency code -Text(price, format: .currency(code: "USD")) -// Output: "$19.99" - -// With locale -Text(price, format: .currency(code: "EUR").locale(Locale(identifier: "de_DE"))) -// Output: "19,99 €" - -// Avoid - manual formatting -Text(String(format: "$%.2f", price)) -``` - -## Percentage Formatting - -```swift -let percentage = 0.856 - -// Correct - with precision -Text(percentage, format: .percent.precision(.fractionLength(1))) -// Output: "85.6%" - -// Without decimal places -Text(percentage, format: .percent.precision(.fractionLength(0))) -// Output: "86%" - -// Avoid - manual calculation -Text(String(format: "%.1f%%", percentage * 100)) -``` - -## Date and Time Formatting - -### Date Formatting - -```swift -let date = Date() - -// Date only -Text(date, format: .dateTime.day().month().year()) -// Output: "Jan 23, 2026" - -// Full date -Text(date, format: .dateTime.day().month(.wide).year()) -// Output: "January 23, 2026" - -// Short date -Text(date, style: .date) -// Output: "1/23/26" -``` - -### Time Formatting - -```swift -let date = Date() - -// Time only -Text(date, format: .dateTime.hour().minute()) -// Output: "2:30 PM" - -// With seconds -Text(date, format: .dateTime.hour().minute().second()) -// Output: "2:30:45 PM" - -// 24-hour format -Text(date, format: .dateTime.hour(.defaultDigits(amPM: .omitted)).minute()) -// Output: "14:30" -``` - -### Relative Date Formatting - -```swift -let futureDate = Date().addingTimeInterval(3600) - -// Relative formatting -Text(futureDate, style: .relative) -// Output: "in 1 hour" - -Text(futureDate, style: .timer) -// Output: "59:59" (counts down) -``` - -## String Searching and Comparison - -### Localized String Comparison - -**Use `localizedStandardContains()` for user-input filtering, not `contains()`.** - -```swift -let searchText = "café" -let items = ["Café Latte", "Coffee", "Tea"] - -// Correct - handles diacritics and case -let filtered = items.filter { $0.localizedStandardContains(searchText) } -// Matches "Café Latte" - -// Wrong - exact match only -let filtered = items.filter { $0.contains(searchText) } -// Might not match "Café Latte" depending on normalization -``` - -**Why**: `localizedStandardContains()` handles case-insensitive, diacritic-insensitive matching appropriate for user-facing search. - -### Case-Insensitive Comparison - -```swift -let text = "Hello World" -let search = "hello" - -// Correct - case-insensitive -if text.localizedCaseInsensitiveContains(search) { - // Match found -} - -// Also correct - for exact comparison -if text.lowercased() == search.lowercased() { - // Equal -} -``` - -### Localized Sorting - -```swift -let names = ["Zoë", "Zara", "Åsa"] - -// Correct - locale-aware sorting -let sorted = names.sorted { $0.localizedStandardCompare($1) == .orderedAscending } -// Output: ["Åsa", "Zara", "Zoë"] - -// Wrong - byte-wise sorting -let sorted = names.sorted() -// Output may not be correct for all locales -``` - -## Attributed Strings - -### Basic Attributed Text - -```swift -// Using Text concatenation -Text("Hello ") - .foregroundStyle(.primary) -+ Text("World") - .foregroundStyle(.blue) - .bold() - -// Using AttributedString -var attributedString = AttributedString("Hello World") -attributedString.foregroundColor = .primary -if let range = attributedString.range(of: "World") { - attributedString[range].foregroundColor = .blue - attributedString[range].font = .body.bold() -} -Text(attributedString) -``` - -### Markdown in Text - -```swift -// Simple markdown -Text("This is **bold** and this is *italic*") - -// With links -Text("Visit [Apple](https://apple.com) for more info") - -// Multiline markdown -Text(""" -# Title -This is a paragraph with **bold** text. -- Item 1 -- Item 2 -""") -``` - -## Text Measurement - -### Measuring Text Height - -```swift -// Wrong (Legacy) - GeometryReader trick -struct MeasuredText: View { - let text: String - @State private var textHeight: CGFloat = 0 - - var body: some View { - Text(text) - .background( - GeometryReader { geometry in - Color.clear - .onAppear { - textWidth = geometry.size.height - } - } - ) - } -} - -// Modern (correct) -struct MeasuredText: View { - let text: String - @State private var textHeight: CGFloat = 0 - - var body: some View { - Text(text) - .onGeometryChange(for: CGFloat.self) { geometry in - geometry.size.height - } action: { newValue in - textHeight = newValue - } - } -} -``` - -## Summary Checklist - -- [ ] Use `.format` parameters with Text instead of `String(format:)` -- [ ] Use `.currency(code:)` for currency formatting -- [ ] Use `.percent` for percentage formatting -- [ ] Use `.dateTime` for date/time formatting -- [ ] Use `localizedStandardContains()` for user-input search -- [ ] Use `localizedStandardCompare()` for locale-aware sorting -- [ ] Use Text concatenation or AttributedString for styled text -- [ ] Use markdown syntax for simple text formatting -- [ ] All formatting respects user's locale and preferences - -**Why**: Modern format parameters are type-safe, localization-aware, and integrate better with SwiftUI's text rendering.