This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
BetterBlue is a native iOS/watchOS app for controlling Hyundai and Kia vehicles via BlueLink/Kia Connect services. Built with SwiftUI, SwiftData, and powered by the BetterBlueKit Swift package.
BetterBlueKit is included as a git submodule in the BetterBlueKit/ directory. This allows local development and testing of BetterBlueKit changes without pushing to GitHub. The project is configured to use the local package via XCLocalSwiftPackageReference.
To initialize or update the submodule after cloning:
git submodule update --init --recursiveImportant: The local submodule has the SwiftLint plugin commented out in Package.swift to avoid build conflicts with the main project. When pushing BetterBlueKit changes upstream, you may need to uncomment the plugin configuration in the standalone repository.
# Open in Xcode
open BetterBlue.xcodeproj
# Build for iOS simulator
xcodebuild -scheme BetterBlue -destination 'platform=iOS Simulator,name=iPhone 15 Pro' build
# Build for device
xcodebuild -scheme BetterBlue -destination 'generic/platform=iOS' build
# Build Watch app
xcodebuild -scheme "BetterBlueWatch Watch App" build
# Build Widget extension
xcodebuild -scheme WidgetExtension buildSwiftLint has been removed from the project to avoid plugin conflicts when BetterBlueKit is used as a local package. You can run SwiftLint manually if needed:
swiftlint lintThe project consists of four targets sharing a common SwiftData container:
- BetterBlue (Main iOS app)
- BetterBlueWatch Watch App (watchOS companion)
- Widget (iOS widgets, lock screen widgets, control center widgets, and Live Activities)
- BetterBlueKit (External Swift package for API communication)
All models are in BetterBlue/Models/:
BBAccount.swift- User accounts with credentials and brand/regionBBVehicle.swift- Vehicle data with status, settings, and climate presetsHTTPLog.swift- HTTP request/response loggingClimatePreset.swift- User-defined climate control presets
SharedModelContainer.swift provides the critical createSharedModelContainer() function:
- Simulator: Uses
/tmp/BetterBlue_Sharedto work around App Group isolation - Device: Uses iCloud sync via
iCloud.com.markschmidt.BetterBluewith App Group fallback (group.com.betterblue.shared) - All targets must use this function to ensure data sharing
- BetterBlueKit Package - Core API clients (Hyundai, Kia, Fake) implementing
APIClientProtocol - CachedAPIClient (
CachedAPIClient.swift) - Request deduplication and 5-second caching wrapper - APIClientFactory (
APIClientFactory.swift) - Creates appropriate API client based on brand - BBAccount Model - SwiftData wrapper managing API client lifecycle, auth tokens, and command execution
- API clients are
@Transient(not persisted) and lazy-initialized on first use CachedAPIClientprevents duplicate simultaneous requests and caches responses- Invalid session/credentials trigger automatic re-initialization via
handleInvalidVehicleSession() - Kia vehicles require
vehicleKeyfield for commands (auto-fetched if missing)
BBVehicle.waitForStatusChange() implements an interruptible polling mechanism:
- Used after commands to wait for vehicle state changes (lock/unlock/climate)
- Polls status at intervals with configurable retry count
- Can be woken up early via
wakeUpStatusWaiters()when status updates arrive - Uses continuation-based async pattern with
StatusWaitingManageractor
Widget/VehicleAppIntents.swift provides Siri shortcuts, Control Center widgets, and app intents:
- Lock/Unlock:
LockVehicleIntent,UnlockVehicleIntentwith status waiting - Climate:
StartClimateIntent,StopClimateIntent(start uses selected climate preset) - Status:
RefreshVehicleStatusIntent,GetVehicleStatusIntent - Live Activities:
VehicleLiveActivityManagercoordinates status updates during long-running operations
Control Center widgets use ControlConfigurationIntent for user-configured vehicle selection.
Testing without real vehicles is supported via Brand.fake:
SwiftDataFakeVehicleProvider(BetterBlue/Utility/) stores fake vehicle state in SwiftDataBBDebugConfigurationstruct enables simulating various failure modes- Fake accounts automatically created for test credentials (see
APIClientFactory.isTestAccount())
BBAccount←→BBVehicle: One-to-many with cascade deleteBBVehicle←→ClimatePreset: One-to-many with cascade delete- Use
safeVehiclesandsafeClimatePresetsproperties to safely unwrap optional relationships
HTTPLogSinkManager singleton coordinates logging across all targets:
- Must be configured with
createSharedModelContainer()and device type - Creates
HTTPLogSinkinstances for API clients to inject logs - Logs viewable in Settings > HTTP Logs
AppSettings manages user preferences (stored in UserDefaults):
preferredDistanceUnit: miles or kilometerspreferredTemperatureUnit: Fahrenheit or Celsius- Used throughout UI and in VehicleEntity for App Intents
Always initialize accounts before use:
try await account.initialize(modelContext: modelContext)This ensures API client is created and login is performed.
BBAccount.updateVehicles() syncs fetched vehicles with SwiftData:
- Updates existing vehicles while preserving UI state (custom names, colors, sort order)
- Creates new vehicles with auto-incrementing sort order
- Removes vehicles no longer returned by API
- Add to
VehicleCommandenum in BetterBlueKit - Implement in BetterBlueKit API clients (Hyundai/Kia/Fake)
- Add convenience method in
BBAccountextensions (seelockVehicle(), etc.) - Create App Intent in
VehicleAppIntents.swiftif needed - Update fake vehicle provider in
SwiftDataFakeVehicleProvider.executeCommand()
- Create widget view conforming to
Widgetprotocol inWidget/ - Register in
BetterBlueWidgetBundle.swift - Use
createSharedModelContainer()in timeline provider - Query
BBVehiclewith proper predicates for filtering
- Check device type detection in
HTTPLogSinkManager.detectMainAppDeviceType() - Verify App Group container is accessible: look for "App Group container not accessible" warnings
- On simulator, check
/tmp/BetterBlue_Shared/BetterBlue.sqlite - Use Diagnostics view (Settings > About) to inspect iCloud sync status
BetterBlue/Views/- SwiftUI views for main appBetterBlue/Views/Components/- Reusable view componentsBetterBlue/Models/- SwiftData modelsBetterBlue/Utility/- Helper classes and utilitiesBetterBlueWatch Watch App/- watchOS app filesWidget/- Widget extensions, App Intents, and Live Activities
- All API operations must pass
modelContext: ModelContextparameter - Use
@MainActorfor SwiftData operations and API client methods - Vehicle commands should use status waiting pattern where state verification is needed
- Climate presets: selected preset takes precedence, fallback to first preset, then default options
- VIN is the primary identifier for vehicles across all operations