## Summary Updates the AI agent instruction files so that AI coding assistants prefer **Swift Concurrency** (`async/await`, `Task`, actors, structured concurrency) for new asynchronous code and **avoid introducing new PromiseKit usage**. PromiseKit remains a legacy dependency in parts of `HomeAssistantAPI` (`HAAPI.swift`), so the guidance also tells agents not to assume a full migration and to convert PromiseKit to `async/await` where practical when they touch that code. Files updated: - `AGENTS.md` — added a **Concurrency** subsection under Common Patterns and a note in the Networking section. - `.github/copilot-instructions.md` — added a **Concurrency** subsection under Swift Conventions and clarified the existing PromiseKit/HAKit notes. (`CLAUDE.md` is a symlink to this file, so it's covered too.) - `.cursorrules` — added a concurrency key rule and a code-style bullet. These are documentation-only changes to AI agent guidance; no app code is affected. ## Screenshots N/A — documentation/instruction files only, no user-facing change. ## Link to pull request in Documentation repository Documentation: home-assistant/companion.home-assistant# ## Any other notes No-op for the build/tests — only Markdown instruction files for AI agents changed. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
9.1 KiB
AI Agent Instructions for Home Assistant iOS
This document provides guidance for AI coding agents (LLMs) working on the Home Assistant for Apple Platforms codebase.
Project Overview
Home Assistant for Apple Platforms is a native Swift companion app for Home Assistant home automation. The primary user interaction is through a WKWebView displaying the Home Assistant web frontend, with native features for notifications, sensors, location tracking, widgets, CarPlay, Apple Watch, and more.
- Language: Swift 5.8+
- Platforms: iOS, watchOS, macOS (Catalyst), CarPlay
- Build System: Xcode 26.2+, CocoaPods, Swift Package Manager
- Workspace: Always open
HomeAssistant.xcworkspace(not the.xcodeproj)
Getting Started
Install Dependencies
bundle install
bundle exec pod install --repo-update
CocoaPods and Swift Package Manager (SPM) manage third-party dependencies. CocoaPods is used for most dependencies, while SPM is used for select packages (e.g., swift-snapshot-testing, WebRTC, ZIPFoundation, firebase-ios-sdk).
Code Signing (for device builds)
Create Configuration/HomeAssistant.overrides.xcconfig (git-ignored):
DEVELOPMENT_TEAM = YourTeamID
BUNDLE_ID_PREFIX = some.bundle.prefix
Project Structure
Sources/
├── App/ # Main iOS app target
├── Shared/ # Shared code across all platforms
├── Watch/ # watchOS-specific code
├── WatchApp/ # watchOS app target
├── MacBridge/ # macOS Catalyst bridge
├── CarPlay/ # CarPlay integration
├── Extensions/ # App Extensions (widgets, notifications, intents)
├── Improv/ # Improv BLE provisioning
├── PushServer/ # Push notification server communication
├── SharedPush/ # Shared push notification handling
├── SharedTesting/ # Shared testing utilities
├── Thread/ # Thread network support
├── Launcher/ # App launcher helper
Tests/
├── App/ # App-level tests
├── Shared/ # Shared module tests
├── UI/ # UI tests
├── Widgets/ # Widget tests
├── Mocks/ # Mock objects for testing
Configuration/ # Xcode build configuration files
fastlane/ # Fastlane automation (build, test, deploy)
Tools/ # Build tools, icon generation
Architecture: The "World" Pattern (Dependency Injection)
This project uses the "World" pattern for dependency injection, inspired by Point-Free's "How to Control the World". This is the most important architectural concept in the codebase.
How It Works
A single global Current variable of type AppEnvironment holds all dependencies as mutable properties:
// Sources/Shared/Environment/Environment.swift
public var Current: AppEnvironment { ... }
public class AppEnvironment {
public var date: () -> Date = Date.init
public var calendar: () -> Calendar = { Calendar.autoupdatingCurrent }
public var servers: ServerManager = ServerManagerImpl()
public var clientEventStore: ClientEventStoreProtocol = ClientEventStore()
// ... many more dependencies
}
Usage in Production Code
Access dependencies through Current:
let now = Current.date()
let server = Current.servers.all.first
Current.Log.info("Something happened")
Usage in Tests
Override dependencies for testing:
Current.date = { Date(timeIntervalSince1970: 1000000) }
Current.servers = FakeServerManager()
⚠️ Critical Rule
Never assign to Current.* properties outside of test code. This is enforced by a custom SwiftLint rule that will fail CI. In production code, only read from Current.
Localization
How Strings Work
- Add strings to the English
.stringsfile:Sources/App/Resources/en.lproj/Localizable.strings - SwiftGen auto-generates type-safe accessors in
Sources/Shared/Resources/SwiftGen/Strings.swiftwhen building the app - Use generated accessors via the
L10nenum:
// In Localizable.strings:
"settings.title" = "Settings";
"sensor.name_%@" = "Sensor: %@";
// In Swift code (auto-generated):
let title = L10n.Settings.title
let name = L10n.Sensor.name("Temperature")
There are multiple string tables:
Localizable.strings→L10nenumCore.strings→CoreStringsenumFrontend.strings→FrontendStringsenum
All string lookup flows through Current.localized.string which handles locale fallback.
Important
: Translations for other languages are managed externally via Lokalise. Only add/modify strings in the
en.lprojfiles.
Code Style & Linting
Automated Linting
# Check for lint issues (does not modify files)
bundle exec fastlane lint
# Auto-fix lint issues (run before committing!)
bundle exec fastlane autocorrect
Always run bundle exec fastlane autocorrect after making changes and before committing.
Linters Used
| Tool | Config File | Purpose |
|---|---|---|
| SwiftFormat | .swiftformat |
Code formatting (120 char max, before-first wrapping) |
| SwiftLint | .swiftlint.yml |
Code quality rules |
| Rubocop | .rubocop.yml |
Ruby/Fastlane code |
| YamlLint | .yamllint.yml |
YAML files |
Key SwiftFormat Rules
- Max line width: 120 characters
- Wrap arguments/parameters/collections:
before-first selfkeyword: only in initializers (--self init-only)- Guard else: same line
- Headers: stripped (no file header comments)
Key SwiftLint Rules
- No
force_castorforce_try - Keep cyclomatic complexity low
- No assigning to
Current.*outside tests - Use
SFSafeSymbolsfor SF Symbol references:
// ❌ Wrong
Image(systemName: "house")
// ✅ Correct
Image(systemSymbol: .house)
Testing
Running Tests
bundle exec fastlane test
Or in Xcode: use the Tests-Unit scheme with ⌘U.
Testing Conventions
- Tests live in
Tests/mirroring the source structure - Mock dependencies by overriding
Current.*properties in test setup - Use
Sources/SharedTesting/for shared test utilities - Tests are excluded from SwiftLint enforcement
Continuous Integration
CI runs on GitHub Actions (.github/workflows/ci.yml):
- Linting: SwiftFormat, SwiftLint, Rubocop, YamlLint
- Unit Tests: Runs the
Tests-Unitscheme - Build Verification: Ensures the app builds cleanly
All lint checks and tests must pass before a PR can be merged.
Common Patterns
Concurrency
Prefer Swift Concurrency (async/await, Task, actors, structured concurrency) for all new asynchronous code.
- Do not introduce new PromiseKit code. PromiseKit is a legacy dependency that the codebase is gradually moving away from. Parts of
HomeAssistantAPI(HAAPI.swift) still use it, so don't assume a full migration — but new work should useasync/awaitinstead ofPromise/Guarantee. - When touching existing PromiseKit code, migrate it to
async/awaitwhere practical rather than extending the PromiseKit usage. - Use
Combineonly where an existing reactive pattern already requires it; otherwise preferasync/awaitandAsyncStream/AsyncSequence. - Annotate SwiftUI-facing view models with
@MainActor.
Networking
Use HAKit (the Home Assistant Swift SDK) for server communication:
- REST API calls via
HAConnection - WebSocket subscriptions for real-time updates
- Connection info managed through
Current.servers - Prefer
async/awaitfor new request flows (see Concurrency); avoid adding new PromiseKit-based calls.
Data Persistence
- GRDB: Primary database for structured data (servers, configurations)
- Realm: Legacy data storage (being migrated)
- UserDefaults: Simple preferences and watch communication
UI Patterns
- SwiftUI: Preferred for new UI (settings, widgets)
- UIKit: Used in older code and where needed for platform APIs
- Use
View.embeddedInHostingController()for SwiftUI-to-UIKit bridging - View models are annotated with
@MainActor - Support both light and dark mode
Assets
- SF Symbols via
SFSafeSymbolslibrary - Material Design Icons available via
MaterialDesignIcons(auto-generated from JSON) - Asset catalogs in
Sources/Shared/Assets/SharedAssets.xcassets
Workflow Summary
- Install dependencies:
bundle install && bundle exec pod install --repo-update - Make your changes in the appropriate
Sources/directory - Add strings to
en.lproj/Localizable.stringsif needed (SwiftGen generates accessors on build) - Run autocorrect:
bundle exec fastlane autocorrect - Run tests:
bundle exec fastlane test - Commit your changes