AGENTS.md: replace the 'no AI attribution footers' rule with a requirement that every agent-authored/co-authored commit end with a Co-Authored-By trailer naming the specific agent/model. Add docs/SWIFT_GUIDELINES.md to the repo layout. docs/SWIFT_GUIDELINES.md: consolidate the team Swift standards (coding style, testing, patterns, security, tooling hooks) into a self-contained project doc, with project notes for Xcode MCP, MainActor/Sendable, and SwiftData gotchas. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
7.0 KiB
Swift Guidelines
Swift coding, testing, patterns, and security standards for OpenAppLock. Agents working on this project must follow these. They are the project-local copy of the team's Swift standards, consolidated here so the repo is self-contained (no dependency on any individual contributor's global config).
Where a rule meets a project specific, a Project note calls it out. General/cross-language principles (immutability, small files, comprehensive error handling, input validation) still apply on top of the Swift specifics below.
1. Coding style
Formatting
- SwiftFormat for auto-formatting, SwiftLint for style enforcement.
swift-formatis bundled with Xcode 16+ as an alternative.
Immutability
- Prefer
letovervar— define everything asletand only change tovarif the compiler requires it. - Use
structwith value semantics by default; useclassonly when identity or reference semantics are needed.- Project note: SwiftData
@Modeltypes are necessarily reference types (BlockingRule,AppList); everything inShared/andLogic/that can be a value type (RuleDraft,RuleSchedule,UsageLedger, snapshots, enums likeRuleKind/Weekday) is.
- Project note: SwiftData
Naming
Follow the Apple API Design Guidelines:
- Clarity at the point of use — omit needless words.
- Name methods and properties for their roles, not their types.
- Use
static letfor constants over global constants.
Error handling
Use typed throws (Swift 6+) and pattern matching:
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:
Sendablevalue types for data crossing isolation boundaries.- Actors for shared mutable state.
- Structured concurrency (
async let,TaskGroup) over unstructuredTask {}.
Project note: the app target defaults to
@MainActorisolation, and the test suites are@MainActor. Data shared with the DeviceActivity / shield extensions through the app group (RuleSnapshot,UsageLedger, theMonitoringPlannaming) must remainSendable.
2. Testing
Framework
Use Swift Testing (import Testing) for new tests — @Test and #expect:
@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.
Project note: SwiftData is the exception. Repeatedly creating
ModelContainers for this schema traps intermittently, so unit tests share one container per process and get a fresh context + data wipe per test viamakeInMemoryContext()(TestSupport.swift). See AGENTS.md → "Gotchas learned the hard way."
Parameterized tests
@Test("Validates formats", arguments: ["json", "xml", "csv"])
func validatesFormat(format: String) throws {
let parser = try Parser(format: format)
#expect(parser.isValid)
}
Coverage
Target 80%+ coverage (unit + integration + critical-flow E2E).
swift test --enable-code-coverage
Project note: this is an Xcode project, not a SwiftPM package — build and run tests through the Xcode MCP tools (
BuildProject,RunAllTests,RunSomeTests), notswift testor rawxcodebuild. The scheme destination must be an iOS simulator or runs hang. UI flows are XCUITest (OpenAppLockUITests) driven by the launch-argument harness in AGENTS.md.
Workflow
Red-green TDD: update docs/RULES_FEATURE_SPEC.md first for behavior changes,
write the failing test, run it (a compile failure counts as red), implement,
re-run focused tests, then the full suite. Run tests often and fail fast.
3. Patterns
Protocol-oriented design
Define small, focused protocols. Use protocol extensions for shared defaults:
protocol Repository: Sendable {
associatedtype Item: Identifiable & Sendable
func find(by id: Item.ID) async throws -> Item?
func save(_ item: Item) async throws
}
Project note: this is how
ScreenTimeAuthorizationis structured — a protocol with a real FamilyControls implementation and a mock, soLogic/stays pure and unit-testable.
Value types
- Use structs for data transfer objects and models.
- Use enums with associated values to model distinct states:
enum LoadState<T: Sendable>: Sendable {
case idle
case loading
case loaded(T)
case failed(Error)
}
Project note:
RuleStatus(disabled / dormant / active(until:) / paused(until:) / upcoming(startsAt:)) is exactly this pattern — status is always derived, never stored.
Actor pattern
Use actors for shared mutable state instead of locks or dispatch queues:
actor Cache<Key: Hashable & Sendable, Value: Sendable> {
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:
struct UserService {
private let repository: any UserRepository
init(repository: any UserRepository = DefaultUserRepository()) {
self.repository = repository
}
}
4. Security
Secret management
- Use Keychain Services for sensitive data (tokens, passwords, keys) — never
UserDefaults. - Use environment variables or
.xcconfigfiles for build-time secrets. - Never hardcode secrets in source — decompilation tools extract them trivially.
let apiKey = ProcessInfo.processInfo.environment["API_KEY"]
guard let apiKey, !apiKey.isEmpty else {
fatalError("API_KEY not configured")
}
Project note: OpenAppLock has no network backend or API keys today.
UserDefaults(and the app-group container) is used only for non-sensitive rule mirroring / stray-shield cleanup — keep it that way; do not put secrets there.
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.
5. Tooling hooks (optional, local)
Contributors may configure PostToolUse hooks in their own
~/.claude/settings.json to run after editing .swift files:
- SwiftFormat — auto-format.
- SwiftLint — lint checks.
- swift build / type-check — catch errors early.
Flag print() statements — use os.Logger or structured logging instead for
production code.