Files
OpenAppLock/docs/SWIFT_GUIDELINES.md
Brendan Chen d92a8588bb docs: require per-agent commit attribution; add Swift guidelines
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>
2026-06-13 15:14:24 -04:00

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-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.
    • Project note: SwiftData @Model types are necessarily reference types (BlockingRule, AppList); everything in Shared/ and Logic/ that can be a value type (RuleDraft, RuleSchedule, UsageLedger, snapshots, enums like RuleKind/Weekday) is.

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 let for 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:

  • Sendable value types for data crossing isolation boundaries.
  • Actors for shared mutable state.
  • Structured concurrency (async let, TaskGroup) over unstructured Task {}.

Project note: the app target defaults to @MainActor isolation, and the test suites are @MainActor. Data shared with the DeviceActivity / shield extensions through the app group (RuleSnapshot, UsageLedger, the MonitoringPlan naming) must remain Sendable.


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 via makeInMemoryContext() (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), not swift test or raw xcodebuild. 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 ScreenTimeAuthorization is structured — a protocol with a real FamilyControls implementation and a mock, so Logic/ 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 .xcconfig files 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.