| name | ios-swift-development |
| description | Use when implementing features, fixing bugs, or refactoring in the GrowWise iOS codebase. Enforces Swift 6 concurrency, MV architecture, SwiftData/CloudKit conventions, and security requirements. |
GrowWise iOS Development Skill
Prerequisites
Before writing any code, verify the current state:
git rev-parse --abbrev-ref HEAD
bd show
cd GrowWisePackage && swift test
Architecture Rules (Non-Negotiable)
MV Pattern only — NO ViewModels:
- Views get data via
@Environment(Service.self) injected from the app root
- Local ephemeral state uses
@State in the view itself
- Never create
class MyViewModel: ObservableObject
- Never use
@StateObject or @ObservedObject
Concurrency:
- UI-bound services must be
@MainActor
- Background operations use Swift
Actor or async/await
- Never use
DispatchQueue or completion handlers
- Structured concurrency only — no
Task.detached without justification
SwiftData:
- All
@Model properties must be Optional or have defaults (CloudKit requirement)
- Never use
ModelConfiguration(isStoredInMemoryOnly: true) in production
- Migrations must be additive — never delete properties from models
Code Quality Requirements
Never acceptable:
let name = plant.name!
let service = object as! DataService
try? service.save()
Always required:
guard let name = plant.name else { return }
do {
try service.save()
} catch {
}
Accessibility (mandatory):
Button("Add Plant") { ... }
.accessibilityIdentifier("addPlantButton")
.accessibilityLabel("Add a new plant")
Logging (never use print):
import OSLog
private let logger = Logger(subsystem: "com.growwise", category: "DataService")
logger.info("Plant saved: \(plant.id, privacy: .public)")
logger.debug("Key rotation: \(keyID, privacy: .private)")
Security Rules
For any change touching GrowWiseServices/Keychain*, Encryption*, SecureEnclave*, KeyRotation*, Token*, or JWT*:
- Never log key material, tokens, or user secrets — even at debug level
- Never store sensitive data in
UserDefaults — use KeychainStorageService
- Always use the
AuditLogger for security-relevant events
- Follow the multi-level fallback pattern from
DataService
Testing
cd GrowWisePackage && swift test
swift test --filter GrowWiseServicesTests
swift test --filter GrowWiseModelsTests
swift test --filter GrowWiseFeatureTests
Tests use Swift Testing framework (@Test, #expect, #require). Never use XCTAssert* in new tests.
Build Verification
xcodebuild \
-workspace GrowWise.xcworkspace \
-scheme GrowWise \
-sdk iphonesimulator \
-destination 'platform=iOS Simulator,name=iPhone 16' \
build \
CODE_SIGNING_ALLOWED=NO
Quality Gates Before Commit
swiftlint lint --strict --config .swiftlint.yml
swiftformat --lint --config .swiftformat GrowWisePackage/Sources GrowWise
cd GrowWisePackage && swift test
Completing Work
- File any follow-up issues:
bd add "description"
- Mark resolved issues:
bd close GW-XXX
- Sync beads state:
bd sync
- Push:
git push
⚠️ Test Quality Standards — Mock Trap Prevention
Critical: These rules apply to ALL test writing, including when using Droid/Factory agents.
The Mock Trap (learn from NM2's 349-test incident)
Mock-only tests prove plumbing works. They do NOT prove real services produce output.
Before writing any test, ask: "If I deleted the mock and used the real service, would this test still pass?"
If no → mock-only gap. You need an integration or contract test too.
Required test types by service kind
| Service Type | Required |
|---|
| Pure logic (calculators, parsers) | Unit tests with known inputs |
@Observable services | Mock-based unit tests (plumbing only) |
| Network/CloudKit API services | Contract test — real parser + fixture JSON |
| Services that produce user-visible output | Integration test — real service + verify non-empty output |
Any service with catch {} or try? | P0 bug first — fix error surfacing before writing tests |
Silent failure audit (run before writing ANY tests)
Scan the real service implementation for:
catch { } or catch { _ = error } — error swallowed, user sees blank screen
try? without logging — failure invisible
guard else { return } — silent no-op
These are P0 bugs. Fix them first. Then write tests.
❌ Reject (shallow)
XCTAssertTrue(app.buttons["Save"].exists)
#expect(vm.items.count == 2)
✅ Require (functional)
app.buttons["Save"].tap()
XCTAssertTrue(app.staticTexts["Saved"].waitForExistence(timeout: 2))
let fixture = Data("""{"status":"ok","count":3}""".utf8)
let result = try JSONDecoder().decode(MyResponse.self, from: fixture)
#expect(result.count == 3)
let svc = PlantService()
let plants = try await svc.fetchPlants(query: "tomato")
#expect(!plants.isEmpty, "Real service must return results")