## 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.3 KiB
GitHub Copilot Instructions for Home Assistant iOS
See AGENTS.md for the full AI agent reference. This file provides GitHub Copilot-specific guidance.
Home Assistant for Apple Platforms is a native iOS, watchOS, and macOS companion app for Home Assistant home automation. The primary iOS UI is a WKWebView (WebViewController) that renders the Home Assistant web frontend; native Swift code wraps it with platform integrations (widgets, CarPlay, Watch, notifications, sensors, etc.).
Build, Lint, and Test Commands
# Initial setup
bundle install
bundle exec pod install --repo-update
# Fix lint issues (run before committing)
bundle exec fastlane autocorrect
# Check lint without fixing
bundle exec fastlane lint
# Run unit tests
bundle exec fastlane test
Always open HomeAssistant.xcworkspace (not the .xcodeproj). Run individual tests in Xcode via the Test navigator (⌘U) or by clicking the diamond next to a test method. The unit test scheme is Tests-Unit.
Code Signing
Debug builds use Automatic provisioning. Create Configuration/HomeAssistant.overrides.xcconfig (gitignored):
DEVELOPMENT_TEAM = YourTeamID
BUNDLE_ID_PREFIX = some.bundle.prefix
Architecture
Source Layout
| Path | Purpose |
|---|---|
Sources/App |
iOS app — AppDelegate, Scenes, WebViewController, Settings, Onboarding |
Sources/Shared |
Cross-platform logic shared by iOS, watchOS, macOS, and extensions |
Sources/Watch / WatchApp |
watchOS app |
Sources/MacBridge |
macOS Catalyst bridge |
Sources/CarPlay |
CarPlay integration |
Sources/Extensions |
App extensions: Widgets, Notification Service/Content, Push Provider, Intents |
Sources/SharedTesting |
Test helpers (snapshot helpers, mock support) |
Tests/ |
Unit and snapshot tests (mirror source structure) |
WKWebView Frontend
WebViewController is the core UI — it loads the Home Assistant web app. Functionality is spread across many WebViewController+*.swift extension files (navigation, gestures, alerts, URL loading, etc.). Native features communicate with the web UI via a JavaScript message bus handled by WebViewExternalMessageHandler (messages typed as WebViewExternalBusMessage / WebViewExternalBusOutgoingMessage in Sources/App/Frontend/ExternalMessageBus/) and custom URL schemes / deep links defined in AppConstants.
Current Environment (Dependency Injection)
This project uses the "World" pattern. The global Current: AppEnvironment object is the DI container. All shared services are accessed through it:
Current.Log.info("message")
Current.api(for: server)?.CallService(...)
Current.database()
Current.servers.all
- Never assign to
Currentoutside of test code — enforced by a SwiftLint custom rule. - In tests, replace
Currentwith a preconfiguredAppEnvironmentto mock dependencies. - SwiftUI-facing view models are annotated
@MainActor.
Dual Persistence: Realm + GRDB
The project uses two database layers:
- Realm (
RealmSwift) — legacy layer, used for older models (actions, zones, sensors, etc.). Access viaCurrent.realm(). - GRDB (
GRDB.swift) — newer layer, accessed viaCurrent.database(). Used for Watch config, CarPlay config, widget config, entity registry, panels, etc.
When adding a new persistent model, prefer GRDB. Implement DatabaseTableProtocol (defines tableName, definedColumns, and createIfNeeded) and register it in DatabaseQueue.tables() in GRDB+Initialization.swift. The protocol's migrateColumns helper auto-handles additive migrations.
HAKit (WebSocket + REST)
HAKit is the library for all Home Assistant server communication. Use HATypedRequest for typed WebSocket/REST calls. Network requests in HomeAssistantAPI (HAAPI.swift) still use PromiseKit for some flows alongside newer async/await. Don't assume the codebase is fully migrated to async/await — but prefer async/await for new request flows and avoid introducing new PromiseKit usage (see Concurrency).
MagicItem — Cross-Platform Action Abstraction
MagicItem (Sources/Shared/MagicItem/MagicItem.swift) is the shared model for items that can appear in Widgets, Watch, CarPlay, and App Shortcuts. It has a type (.script, .scene, .entity, .action, .folder, .assistPipeline) and an optional action override. ItemType.rawValue is persisted (Codable), so never change existing raw values.
Swift Conventions
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 the codebase is moving away from — parts of HomeAssistantAPI (HAAPI.swift) still use it, so don't assume a full migration, but new work should use async/await instead of Promise/Guarantee. When editing existing PromiseKit code, migrate it to async/await where practical rather than extending the PromiseKit usage. Use Combine only where an existing reactive pattern already requires it.
- Use Swift's modern language features (
async/await, structured concurrency,Combinewhere appropriate). Avoid PromiseKit in new code — see Concurrency above. - SwiftUI preferred for new UI; UIKit where existing patterns require it.
- Prefer value types (structs) over reference types (classes) when appropriate.
- Use proper access control (
private,fileprivate,internal,public). - Avoid force unwrapping (
!) and force casting unless absolutely necessary. - Use guard statements for early returns.
- Keep cyclomatic complexity low — extract helper methods for complex logic.
- SwiftUI-facing view models are annotated
@MainActorat the type level.
Key Conventions
Localized Strings
All user-visible strings come from SwiftGen-generated enums — never use hardcoded string literals. Add new strings to Sources/App/Resources/en.lproj/Localizable.strings; the type-safe accessors are regenerated automatically as an Xcode build phase (SwiftGen runs via CocoaPods at Pods/SwiftGen/bin/swiftgen).
L10n.*— app-specific strings fromLocalizable.stringsCoreStrings.*— Home Assistant core translationsFrontendStrings.*— Home Assistant frontend translations
SFSymbols
Use SFSafeSymbols for type-safe SF Symbol references:
// ❌ Avoid
Image(systemName: "house")
// ✅ Prefer
Image(systemSymbol: .house)
Logging
Use Current.Log (XCGLogger) — never print or NSLog:
Current.Log.info("Connected to \(server.info.name)")
Current.Log.error("Failed: \(error.localizedDescription)")
Current.Log.verbose("Debug detail")
Icons
Use MaterialDesignIcons enum for all HA domain/entity icons. Icons are named with the Icon suffix (e.g., .lightbulbIcon, .scriptTextOutlineIcon). The Domain.icon(deviceClass:state:) method provides domain-appropriate icons.
with() Helper
with(_:update:) in Sources/Shared/Common/With.swift is used for fluent inline initialization:
public lazy var webhooks = with(WebhookManager()) {
$0.register(responseHandler: ..., for: .updateSensors)
}
SwiftUI ↔ UIKit Bridge
Use View.embeddedInHostingController() (defined in Sources/Shared/Common/Extensions/View+HA.swift) to wrap SwiftUI views for UIKit presentation — it injects ViewControllerProvider.
Snapshot Testing
New SwiftUI views should have snapshot tests using helpers from SharedTesting:
import SharedTesting
func testMyView() {
assertLightDarkSnapshots(of: MyView()) // tests both light and dark mode
}
Tests in Tests/ mirror the source structure.
Dependencies
Dependencies are managed via both CocoaPods (most deps, Podfile) and Swift Package Manager (e.g., swift-snapshot-testing, WebRTC, ZIPFoundation, firebase-ios-sdk). Add new dependencies sparingly.
Linters Configuration
- SwiftFormat (
.swiftformat): 120-char line width,before-firstargument wrapping,selfonly in initializers, guard-else on same line. - SwiftLint (
.swiftlint.yml): enabled rules includecyclomatic_complexity,force_cast,force_try,unused_optional_binding,weak_delegate; custom rules enforce noCurrentassignment outside tests and requireSFSafeSymbols. - Rubocop (
.rubocop.yml): for Fastlane/Ruby files. - YamlLint (
.yamllint.yml): for YAML files.
Continuous Integration
CI runs on GitHub Actions:
- Linting (SwiftFormat, SwiftLint, Rubocop, YamlLint)
- Unit tests (scheme
Tests-Unit) - Build verification
- Deployment to App Store Connect (on release)
Pull Requests
- Follow the pull request template.
- Include screenshots for UI changes (light and dark mode).
- Ensure
bundle exec fastlane lintpasses before submitting. - Update documentation if adding/changing functionality.
- Translations: only edit English sources (
en.lproj/Localizable.strings). All other locales are synced from lokalise.com — do not edit non-English.stringsfiles manually. - Link related documentation PRs in the
companion.home-assistantrepository.