From 6792e9173518797123c18ac0306df92dbeacb2be Mon Sep 17 00:00:00 2001 From: Maksim Dimitrov Date: Tue, 17 Feb 2026 15:43:14 +0200 Subject: [PATCH 1/3] feat: add Swift language-specific rules Add 5 rule files for Swift following the established pattern used by TypeScript, Python, and Go rule sets. Covers Swift 6 strict concurrency, Swift Testing framework, protocol-oriented patterns, Keychain-based secret management, and SwiftFormat/SwiftLint hooks. --- rules/README.md | 5 ++- rules/swift/coding-style.md | 47 ++++++++++++++++++++++++++ rules/swift/hooks.md | 20 +++++++++++ rules/swift/patterns.md | 66 +++++++++++++++++++++++++++++++++++++ rules/swift/security.md | 33 +++++++++++++++++++ rules/swift/testing.md | 45 +++++++++++++++++++++++++ 6 files changed, 215 insertions(+), 1 deletion(-) create mode 100644 rules/swift/coding-style.md create mode 100644 rules/swift/hooks.md create mode 100644 rules/swift/patterns.md create mode 100644 rules/swift/security.md create mode 100644 rules/swift/testing.md diff --git a/rules/README.md b/rules/README.md index 57de9d38..51dffbad 100644 --- a/rules/README.md +++ b/rules/README.md @@ -17,7 +17,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. @@ -32,6 +33,7 @@ rules/ ./install.sh typescript ./install.sh python ./install.sh golang +./install.sh swift # Install multiple languages at once ./install.sh typescript python @@ -53,6 +55,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. From f5149d84ec241764be53d168718442ace55cc606 Mon Sep 17 00:00:00 2001 From: Maksim Dimitrov Date: Tue, 17 Feb 2026 15:52:15 +0200 Subject: [PATCH 2/3] feat: add swiftui-patterns skill Add comprehensive SwiftUI skill covering @Observable state management, view composition, type-safe NavigationStack routing, performance optimization with lazy containers, and modern preview patterns. --- skills/swiftui-patterns/SKILL.md | 257 +++++++++++++++++++++++++++++++ 1 file changed, 257 insertions(+) create mode 100644 skills/swiftui-patterns/SKILL.md diff --git a/skills/swiftui-patterns/SKILL.md b/skills/swiftui-patterns/SKILL.md new file mode 100644 index 00000000..ada05e33 --- /dev/null +++ b/skills/swiftui-patterns/SKILL.md @@ -0,0 +1,257 @@ +--- +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() + + 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] + + static func == (lhs: Self, rhs: Self) -> Bool { + lhs.dataPoints.count == rhs.dataPoints.count + } + + var body: some View { + // Complex chart rendering + } +} +``` + +## Previews + +Use `#Preview` macro with inline mock data for fast iteration: + +```swift +#Preview("Empty state") { + ItemListView() + .environment(ItemListViewModel(repository: EmptyMockRepository())) +} + +#Preview("Loaded") { + ItemListView() + .environment(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. From 9d8e4b5af831cbe74cdda6a2ffd841b2fa4cc526 Mon Sep 17 00:00:00 2001 From: Maksim Dimitrov Date: Tue, 17 Feb 2026 17:04:31 +0200 Subject: [PATCH 3/3] fix: correct SwiftUI skill ViewModel injection and Equatable comparison Fix ItemListView to accept viewModel via init with default parameter so previews can inject mocks. Fix ExpensiveChartView Equatable to compare full array instead of only count. --- skills/swiftui-patterns/SKILL.md | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/skills/swiftui-patterns/SKILL.md b/skills/swiftui-patterns/SKILL.md index ada05e33..d0972c37 100644 --- a/skills/swiftui-patterns/SKILL.md +++ b/skills/swiftui-patterns/SKILL.md @@ -59,7 +59,11 @@ final class ItemListViewModel { ```swift struct ItemListView: View { - @State private var viewModel = ItemListViewModel() + @State private var viewModel: ItemListViewModel + + init(viewModel: ItemListViewModel = ItemListViewModel()) { + _viewModel = State(initialValue: viewModel) + } var body: some View { List(viewModel.items) { item in @@ -215,10 +219,10 @@ For views with expensive bodies, conform to `Equatable` to skip unnecessary re-r ```swift struct ExpensiveChartView: View, Equatable { - let dataPoints: [DataPoint] + let dataPoints: [DataPoint] // DataPoint must conform to Equatable static func == (lhs: Self, rhs: Self) -> Bool { - lhs.dataPoints.count == rhs.dataPoints.count + lhs.dataPoints == rhs.dataPoints } var body: some View { @@ -233,13 +237,11 @@ Use `#Preview` macro with inline mock data for fast iteration: ```swift #Preview("Empty state") { - ItemListView() - .environment(ItemListViewModel(repository: EmptyMockRepository())) + ItemListView(viewModel: ItemListViewModel(repository: EmptyMockRepository())) } #Preview("Loaded") { - ItemListView() - .environment(ItemListViewModel(repository: PopulatedMockRepository())) + ItemListView(viewModel: ItemListViewModel(repository: PopulatedMockRepository())) } ```