diff --git a/rules/README.md b/rules/README.md index 10d8ef23..0cb01485 100644 --- a/rules/README.md +++ b/rules/README.md @@ -16,7 +16,8 @@ rules/ │ └── security.md ├── typescript/ # TypeScript/JavaScript specific ├── python/ # Python specific -└── golang/ # Go specific +├── golang/ # Go specific +└── swift/ # Swift specific ``` - **common/** contains universal principles — no language-specific code examples. @@ -31,6 +32,7 @@ rules/ ./install.sh typescript ./install.sh python ./install.sh golang +./install.sh swift # Install multiple languages at once ./install.sh typescript python @@ -52,6 +54,7 @@ cp -r rules/common ~/.claude/rules/common cp -r rules/typescript ~/.claude/rules/typescript cp -r rules/python ~/.claude/rules/python cp -r rules/golang ~/.claude/rules/golang +cp -r rules/swift ~/.claude/rules/swift # Attention ! ! ! Configure according to your actual project requirements; the configuration here is for reference only. ``` diff --git a/rules/swift/coding-style.md b/rules/swift/coding-style.md new file mode 100644 index 00000000..d9fc38d7 --- /dev/null +++ b/rules/swift/coding-style.md @@ -0,0 +1,47 @@ +--- +paths: + - "**/*.swift" + - "**/Package.swift" +--- +# Swift Coding Style + +> This file extends [common/coding-style.md](../common/coding-style.md) with Swift specific content. + +## Formatting + +- **SwiftFormat** for auto-formatting, **SwiftLint** for style enforcement +- `swift-format` is bundled with Xcode 16+ as an alternative + +## Immutability + +- Prefer `let` over `var` — define everything as `let` and only change to `var` if the compiler requires it +- Use `struct` with value semantics by default; use `class` only when identity or reference semantics are needed + +## Naming + +Follow [Apple API Design Guidelines](https://www.swift.org/documentation/api-design-guidelines/): + +- Clarity at the point of use — omit needless words +- Name methods and properties for their roles, not their types +- Use `static let` for constants over global constants + +## Error Handling + +Use typed throws (Swift 6+) and pattern matching: + +```swift +func load(id: String) throws(LoadError) -> Item { + guard let data = try? read(from: path) else { + throw .fileNotFound(id) + } + return try decode(data) +} +``` + +## Concurrency + +Enable Swift 6 strict concurrency checking. Prefer: + +- `Sendable` value types for data crossing isolation boundaries +- Actors for shared mutable state +- Structured concurrency (`async let`, `TaskGroup`) over unstructured `Task {}` diff --git a/rules/swift/hooks.md b/rules/swift/hooks.md new file mode 100644 index 00000000..0fbde366 --- /dev/null +++ b/rules/swift/hooks.md @@ -0,0 +1,20 @@ +--- +paths: + - "**/*.swift" + - "**/Package.swift" +--- +# Swift Hooks + +> This file extends [common/hooks.md](../common/hooks.md) with Swift specific content. + +## PostToolUse Hooks + +Configure in `~/.claude/settings.json`: + +- **SwiftFormat**: Auto-format `.swift` files after edit +- **SwiftLint**: Run lint checks after editing `.swift` files +- **swift build**: Type-check modified packages after edit + +## Warning + +Flag `print()` statements — use `os.Logger` or structured logging instead for production code. diff --git a/rules/swift/patterns.md b/rules/swift/patterns.md new file mode 100644 index 00000000..b03b0baf --- /dev/null +++ b/rules/swift/patterns.md @@ -0,0 +1,66 @@ +--- +paths: + - "**/*.swift" + - "**/Package.swift" +--- +# Swift Patterns + +> This file extends [common/patterns.md](../common/patterns.md) with Swift specific content. + +## Protocol-Oriented Design + +Define small, focused protocols. Use protocol extensions for shared defaults: + +```swift +protocol Repository: Sendable { + associatedtype Item: Identifiable & Sendable + func find(by id: Item.ID) async throws -> Item? + func save(_ item: Item) async throws +} +``` + +## Value Types + +- Use structs for data transfer objects and models +- Use enums with associated values to model distinct states: + +```swift +enum LoadState: Sendable { + case idle + case loading + case loaded(T) + case failed(Error) +} +``` + +## Actor Pattern + +Use actors for shared mutable state instead of locks or dispatch queues: + +```swift +actor Cache { + private var storage: [Key: Value] = [:] + + func get(_ key: Key) -> Value? { storage[key] } + func set(_ key: Key, value: Value) { storage[key] = value } +} +``` + +## Dependency Injection + +Inject protocols with default parameters — production uses defaults, tests inject mocks: + +```swift +struct UserService { + private let repository: any UserRepository + + init(repository: any UserRepository = DefaultUserRepository()) { + self.repository = repository + } +} +``` + +## References + +See skill: `swift-actor-persistence` for actor-based persistence patterns. +See skill: `swift-protocol-di-testing` for protocol-based DI and testing. diff --git a/rules/swift/security.md b/rules/swift/security.md new file mode 100644 index 00000000..878503ae --- /dev/null +++ b/rules/swift/security.md @@ -0,0 +1,33 @@ +--- +paths: + - "**/*.swift" + - "**/Package.swift" +--- +# Swift Security + +> This file extends [common/security.md](../common/security.md) with Swift specific content. + +## Secret Management + +- Use **Keychain Services** for sensitive data (tokens, passwords, keys) — never `UserDefaults` +- Use environment variables or `.xcconfig` files for build-time secrets +- Never hardcode secrets in source — decompilation tools extract them trivially + +```swift +let apiKey = ProcessInfo.processInfo.environment["API_KEY"] +guard let apiKey, !apiKey.isEmpty else { + fatalError("API_KEY not configured") +} +``` + +## Transport Security + +- App Transport Security (ATS) is enforced by default — do not disable it +- Use certificate pinning for critical endpoints +- Validate all server certificates + +## Input Validation + +- Sanitize all user input before display to prevent injection +- Use `URL(string:)` with validation rather than force-unwrapping +- Validate data from external sources (APIs, deep links, pasteboard) before processing diff --git a/rules/swift/testing.md b/rules/swift/testing.md new file mode 100644 index 00000000..9a1b0127 --- /dev/null +++ b/rules/swift/testing.md @@ -0,0 +1,45 @@ +--- +paths: + - "**/*.swift" + - "**/Package.swift" +--- +# Swift Testing + +> This file extends [common/testing.md](../common/testing.md) with Swift specific content. + +## Framework + +Use **Swift Testing** (`import Testing`) for new tests. Use `@Test` and `#expect`: + +```swift +@Test("User creation validates email") +func userCreationValidatesEmail() throws { + #expect(throws: ValidationError.invalidEmail) { + try User(email: "not-an-email") + } +} +``` + +## Test Isolation + +Each test gets a fresh instance — set up in `init`, tear down in `deinit`. No shared mutable state between tests. + +## Parameterized Tests + +```swift +@Test("Validates formats", arguments: ["json", "xml", "csv"]) +func validatesFormat(format: String) throws { + let parser = try Parser(format: format) + #expect(parser.isValid) +} +``` + +## Coverage + +```bash +swift test --enable-code-coverage +``` + +## Reference + +See skill: `swift-protocol-di-testing` for protocol-based dependency injection and mock patterns with Swift Testing. diff --git a/skills/swiftui-patterns/SKILL.md b/skills/swiftui-patterns/SKILL.md new file mode 100644 index 00000000..d0972c37 --- /dev/null +++ b/skills/swiftui-patterns/SKILL.md @@ -0,0 +1,259 @@ +--- +name: swiftui-patterns +description: SwiftUI architecture patterns, state management with @Observable, view composition, navigation, performance optimization, and modern iOS/macOS UI best practices. +--- + +# SwiftUI Patterns + +Modern SwiftUI patterns for building declarative, performant user interfaces on Apple platforms. Covers the Observation framework, view composition, type-safe navigation, and performance optimization. + +## When to Activate + +- Building SwiftUI views and managing state (`@State`, `@Observable`, `@Binding`) +- Designing navigation flows with `NavigationStack` +- Structuring view models and data flow +- Optimizing rendering performance for lists and complex layouts +- Working with environment values and dependency injection in SwiftUI + +## State Management + +### Property Wrapper Selection + +Choose the simplest wrapper that fits: + +| Wrapper | Use Case | +|---------|----------| +| `@State` | View-local value types (toggles, form fields, sheet presentation) | +| `@Binding` | Two-way reference to parent's `@State` | +| `@Observable` class + `@State` | Owned model with multiple properties | +| `@Observable` class (no wrapper) | Read-only reference passed from parent | +| `@Bindable` | Two-way binding to an `@Observable` property | +| `@Environment` | Shared dependencies injected via `.environment()` | + +### @Observable ViewModel + +Use `@Observable` (not `ObservableObject`) — it tracks property-level changes so SwiftUI only re-renders views that read the changed property: + +```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()) ?? [] + } +} +``` + +### View Consuming the ViewModel + +```swift +struct 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 Injection + +Replace `@EnvironmentObject` with `@Environment`: + +```swift +// Inject +ContentView() + .environment(authManager) + +// Consume +struct ProfileView: View { + @Environment(AuthManager.self) private var auth + + var body: some View { + Text(auth.currentUser?.name ?? "Guest") + } +} +``` + +## View Composition + +### Extract Subviews to Limit Invalidation + +Break views into small, focused structs. When state changes, only the subview reading that state re-renders: + +```swift +struct OrderView: View { + @State private var viewModel = OrderViewModel() + + var body: some View { + VStack { + OrderHeader(title: viewModel.title) + OrderItemList(items: viewModel.items) + OrderTotal(total: viewModel.total) + } + } +} +``` + +### ViewModifier for Reusable Styling + +```swift +struct CardModifier: ViewModifier { + func body(content: Content) -> some View { + content + .padding() + .background(.regularMaterial) + .clipShape(RoundedRectangle(cornerRadius: 12)) + } +} + +extension View { + func cardStyle() -> some View { + modifier(CardModifier()) + } +} +``` + +## Navigation + +### Type-Safe NavigationStack + +Use `NavigationStack` with `NavigationPath` for programmatic, type-safe routing: + +```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) + } +} +``` + +## Performance + +### Use Lazy Containers for Large Collections + +`LazyVStack` and `LazyHStack` create views only when visible: + +```swift +ScrollView { + LazyVStack(spacing: 8) { + ForEach(items) { item in + ItemRow(item: item) + } + } +} +``` + +### Stable Identifiers + +Always use stable, unique IDs in `ForEach` — avoid using array indices: + +```swift +// Use Identifiable conformance or explicit id +ForEach(items, id: \.stableID) { item in + ItemRow(item: item) +} +``` + +### Avoid Expensive Work in body + +- Never perform I/O, network calls, or heavy computation inside `body` +- Use `.task {}` for async work — it cancels automatically when the view disappears +- Use `.sensoryFeedback()` and `.geometryGroup()` sparingly in scroll views +- Minimize `.shadow()`, `.blur()`, and `.mask()` in lists — they trigger offscreen rendering + +### Equatable Conformance + +For views with expensive bodies, conform to `Equatable` to skip unnecessary re-renders: + +```swift +struct 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 + } +} +``` + +## Previews + +Use `#Preview` macro with inline mock data for fast iteration: + +```swift +#Preview("Empty state") { + ItemListView(viewModel: ItemListViewModel(repository: EmptyMockRepository())) +} + +#Preview("Loaded") { + ItemListView(viewModel: ItemListViewModel(repository: PopulatedMockRepository())) +} +``` + +## Anti-Patterns to Avoid + +- Using `ObservableObject` / `@Published` / `@StateObject` / `@EnvironmentObject` in new code — migrate to `@Observable` +- Putting async work directly in `body` or `init` — use `.task {}` or explicit load methods +- Creating view models as `@State` inside child views that don't own the data — pass from parent instead +- Using `AnyView` type erasure — prefer `@ViewBuilder` or `Group` for conditional views +- Ignoring `Sendable` requirements when passing data to/from actors + +## References + +See skill: `swift-actor-persistence` for actor-based persistence patterns. +See skill: `swift-protocol-di-testing` for protocol-based DI and testing with Swift Testing.