# AXPref Macro System Documentation ## 1. Introduction & Overview ### What is AXPref? The AXPref macro system is a modern, Swift-first approach to application settings and preferences management. It provides a declarative way to define settings that are type-safe, observable, and automatically synchronized across your application. ### Key Benefits **Type Safety**: Settings are strongly typed at compile time, eliminating common runtime errors from incorrect preference keys or type mismatches. **Automatic Observability**: Built on Swift's Observation framework, settings automatically notify observers when values change, integrating seamlessly with SwiftUI and modern Swift concurrency. **Intelligent Caching**: The system includes sophisticated caching mechanisms that reduce preference store access while maintaining consistency across your application. **Streaming Support**: Settings provide `AsyncStream` interfaces for real-time updates, perfect for reactive programming patterns. **Cross-Platform Sync**: Built-in support for syncing settings across devices, including watchOS companion apps. **Rich Type Support**: Leverages Swift's Codable protocol with `AXMigratingEncoder/Decoder` to seamlessly store complex structs, enums, and custom types while maintaining human-readable values in the underlying preference store. This enables you to use sophisticated data models for your settings while keeping the stored preferences inspectable and debuggable. ### Architecture Overview The AXPref system consists of several key components: - **`@AXPref` macro**: Transforms property declarations into full-featured setting records - **`AXSettingRecord`**: The core observable setting implementation with caching and streaming - **`AXSettingsStore` protocol**: Pluggable storage backend abstraction - **`AXMigratingEncoder/Decoder`**: Handles encoding/decoding of complex types to readable preference values - **Attributes system**: SwiftUI-inspired metadata for extensible setting configuration - **Runtime introspection**: Query and enumerate settings by traits and metadata ### Basic Example ```swift @AXSettingsGroup(domain: "com.myapp.settings") class AppSettings { @AXPref var themeMode: ThemeMode = .automatic @AXPref(traits: [.watchSync]) var notificationsEnabled: Bool = true @AXPref var userProfile: UserProfile = UserProfile(name: "Default", preferences: .standard) } ``` This simple declaration creates fully-featured settings that are: - Automatically persisted to your chosen storage backend - Observable for UI binding and reactive programming - Cached for optimal performance - Available as async streams for real-time updates - Encoded as readable values in preferences (even complex custom types) ## 2. Basic Usage ### Simple Setting Declarations The `@AXPref` macro transforms property declarations into fully-featured settings. By default, keys are automatically generated from your property names: ```swift @AXSettingsGroup(domain: "com.myapp.settings") class AppSettings { @AXPref var userName: String = "Guest" // Key: "AppSettingsUserName" @AXPref var maxItems: Int = 50 // Key: "AppSettingsMaxItems" @AXPref var debugMode: Bool = false // Key: "AppSettingsDebugMode" } ``` ### Supported Types AXPref supports three categories of types out of the box: **Basic Codable Types**: Any type conforming to `Codable` can be stored directly: ```swift @AXPref var lastSyncDate: Date = Date() @AXPref var userSettings: UserConfiguration = UserConfiguration.default ``` **RawRepresentable Types**: Enums and other `RawRepresentable` types with `Codable` raw values: ```swift enum Theme: String, Codable { case light, dark, automatic } @AXPref var theme: Theme = .automatic ``` **RawRepresentable + Codable Types**: Types that are both `RawRepresentable` and `Codable` for maximum flexibility: ```swift enum Priority: Int, Codable { case low = 0, medium = 1, high = 2 } @AXPref var defaultPriority: Priority = .medium ``` ### Key Generation and Prefixes The `@AXSettingsGroup` macro supports automatic key generation and prefixing: **Default behavior**: Keys are generated by combining the class name with the property name in pascal case: ```swift @AXSettingsGroup(domain: "com.myapp.settings") class AppSettings { @AXPref var timeoutInterval: TimeInterval = 30.0 // Uses "AppSettingsTimeoutInterval" as the key } ``` **Customizing the key prefix**: Use the `keyPrefix` parameter to override the default class name prefix: ```swift @AXSettingsGroup(domain: "com.myapp.ui", keyPrefix: "UI") class InterfaceSettings { @AXPref var animationDuration: Double = 0.3 // Uses "UIAnimationDuration" as the key @AXPref var reducedMotion: Bool = false // Uses "UIReducedMotion" as the key } ``` **Overriding with explicit keys**: When you need specific key names, use the `key` parameter: ```swift @AXSettingsGroup(domain: "com.myapp.settings") class AppSettings { @AXPref var timeoutInterval: TimeInterval = 30.0 // Uses generated key: "AppSettingsTimeoutInterval" @AXPref(key: "legacy_timeout_setting") var legacyTimeout: Int = 60 // Uses explicit key: "legacy_timeout_setting" } ``` ### Multiple Settings Groups You can organize settings into multiple groups with different domains and key prefixes: ```swift @AXSettingsGroup(domain: "com.myapp.network", keyPrefix: "Network") class NetworkSettings { @AXPref var timeoutSeconds: Int = 30 // Key: "NetworkTimeoutSeconds" @AXPref var retryCount: Int = 3 // Key: "NetworkRetryCount" } @AXSettingsGroup(domain: "com.myapp.ui", keyPrefix: "Interface") class UISettings { @AXPref var animationSpeed: Double = 1.0 // Key: "InterfaceAnimationSpeed" @AXPref var showDebugInfo: Bool = false // Key: "InterfaceShowDebugInfo" } ``` ### Working with Settings Once declared, settings behave like normal Swift properties: ```swift let networkSettings = NetworkSettings() // Read values print("Timeout: \(networkSettings.timeoutSeconds)") // Write values (automatically persists and notifies observers) networkSettings.timeoutSeconds = 60 // Use in SwiftUI (automatic UI updates when settings change) struct SettingsView: View { @State private var settings = UISettings() var body: some View { VStack { Slider(value: $settings.animationSpeed, in: 0.1...3.0) Toggle("Show Debug Info", isOn: $settings.showDebugInfo) } } } ``` ## 3. AXSettingsStore Protocol ### Purpose and Abstraction The `AXSettingsStore` protocol provides a pluggable abstraction for where and how your settings are actually stored. This design allows you to: - Switch storage backends without changing your settings declarations - Use different stores for different types of settings - Create mock stores for testing - Implement custom storage solutions (cloud sync, encrypted storage, etc.) ### Settings Groups as Interface Definitions A key architectural principle of the AXPref system is that your settings group classes define the **shape and interface** of your settings while completely abstracting away **where** they are actually stored. This separation of concerns enables powerful patterns: ```swift @AXSettingsGroup(domain: "com.myapp.accessibility") class AccessibilitySettings { @AXPref var voiceOverEnabled: Bool = false @AXPref var speechRate: Double = 0.5 @AXPref var soundEffectsEnabled: Bool = true @AXPref var announceNotifications: Bool = true } ``` This single class definition can be used across multiple contexts with different storage backends: **Main Settings App**: Uses the standard Core Foundation preferences store ```swift // In your main settings app let settings = AccessibilitySettings() // Uses AXCoreSettingsStore.shared by default ``` **Watch Bridge App**: Uses the NanoPreferences store to sync to the paired watch ```swift // In your watch companion/bridge app let watchSettings = AXPreferenceContext.$store.withValue(NPSettingsStore()) { AccessibilitySettings() } // Same interface, but writes to the paired watch ``` **Shared UI Components**: The same UI can work with both contexts ```swift struct AccessibilitySettingsView: View { let settings: AccessibilitySettings var body: some View { Form { Toggle("VoiceOver", isOn: $settings.voiceOverEnabled) VStack(alignment: .leading) { Text("Speech Rate") Slider(value: $settings.speechRate, in: 0.1...1.0) } Toggle("Sound Effects", isOn: $settings.soundEffectsEnabled) Toggle("Announce Notifications", isOn: $settings.announceNotifications) } } } // Use the same UI in different apps: // Main settings app let mainAppView = AccessibilitySettingsView(settings: AccessibilitySettings()) // Watch bridge app let bridgeSettings = AXPreferenceContext.$store.withValue(NPSettingsStore()) { AccessibilitySettings() } let bridgeAppView = AccessibilitySettingsView(settings: bridgeSettings) ``` This pattern eliminates code duplication and ensures consistency between your main app settings and companion app settings interfaces. ### Protocol Overview ```swift public protocol AXSettingsStore { func value(forKey key: AXSettingsKey) -> AnyHashable? func set(value: AnyHashable?, forKey key: AXSettingsKey) func observeWithToken(key: AXSettingsKey, onChange: @escaping () -> Void) -> UUID func removeObserver(token: UUID) } ``` ### Built-in Implementations **AXCoreSettingsStore**: The primary implementation backed by Core Foundation preferences: ```swift @AXSettingsGroup(domain: "com.myapp.settings") class AppSettings { // Uses AXCoreSettingsStore by default via AXPreferenceContext @AXPref var userName: String = "Guest" } ``` **NPSettingsStore**: Specialized store for watchOS synchronization. Used in bridge (watch companion) apps to write settings to the paired watch from the phone: ```swift // In your watch companion app @AXSettingsGroup(domain: "com.myapp.watch") class WatchSyncSettings { @AXPref var notificationsEnabled: Bool = true @AXPref var workoutAutoStart: Bool = false } ``` ### Using Alternative Stores with AXPreferenceContext The most flexible way to use alternative stores is through `AXPreferenceContext.$store.withValue()`, which temporarily overrides the store context during initialization: #### Basic Alternative Store Usage ```swift // Create settings with a custom store let customStore = MyCustomSettingsStore() let settingsWithCustomStore = AXPreferenceContext.$store.withValue(customStore) { AppSettings() } ``` #### Watch Synchronization ```swift // Configure settings for watch syncing let watchSettings = AXPreferenceContext.$store.withValue(NPSettingsStore()) { WatchSyncSettings() } ``` #### Testing with In-Memory Store ```swift // Create settings that don't persist to disk (perfect for testing) let testSettings = AXPreferenceContext.$store.withValue(AXInMemorySettingsStore()) { AppSettings() } // Or use the convenience method let testSettings = AppSettings.testSettings() ``` #### Multiple Store Contexts ```swift // Different settings groups can use different stores let localSettings = AXPreferenceContext.$store.withValue(AXCoreSettingsStore.shared) { LocalAppSettings() } let cloudSettings = AXPreferenceContext.$store.withValue(CloudSyncSettingsStore()) { UserCloudSettings() } let watchSettings = AXPreferenceContext.$store.withValue(NPSettingsStore()) { WatchSyncSettings() } ``` ### Custom Storage Backends You can implement your own storage backend for specialized needs: ```swift class CloudSyncSettingsStore: AXSettingsStore { private var storage: [String: AnyHashable] = [:] private var observers: [UUID: (AXSettingsKey, () -> Void)] = [:] func value(forKey key: AXSettingsKey) -> AnyHashable? { let fullKey = "\(key.domain).\(key.key)" // Check local cache first if let cached = storage[fullKey] { return cached } // Fetch from cloud service let cloudValue = fetchFromCloud(key: fullKey) storage[fullKey] = cloudValue return cloudValue } func set(value: AnyHashable?, forKey key: AXSettingsKey) { let fullKey = "\(key.domain).\(key.key)" storage[fullKey] = value // Sync to cloud asynchronously Task { await syncToCloud(key: fullKey, value: value) } // Notify observers notifyObservers(for: key) } func observeWithToken(key: AXSettingsKey, onChange: @escaping () -> Void) -> UUID { let token = UUID() observers[token] = (key, onChange) return token } func removeObserver(token: UUID) { observers.removeValue(forKey: token) } private func notifyObservers(for key: AXSettingsKey) { for (_, (observedKey, callback)) in observers { if observedKey.domain == key.domain && observedKey.key == key.key { callback() } } } // Implementation details for cloud sync... private func fetchFromCloud(key: String) -> AnyHashable? { /* ... */ } private func syncToCloud(key: String, value: AnyHashable?) async { /* ... */ } } ``` ### Testing with Mock Stores For unit testing, create a simple in-memory mock store: ```swift class MockSettingsStore: AXSettingsStore { private var storage: [String: AnyHashable] = [:] private var observers: [UUID: () -> Void] = [:] func value(forKey key: AXSettingsKey) -> AnyHashable? { return storage["\(key.domain).\(key.key)"] } func set(value: AnyHashable?, forKey key: AXSettingsKey) { let fullKey = "\(key.domain).\(key.key)" storage[fullKey] = value // Notify all observers for simplicity observers.values.forEach { $0() } } func observeWithToken(key: AXSettingsKey, onChange: @escaping () -> Void) -> UUID { let token = UUID() observers[token] = onChange return token } func removeObserver(token: UUID) { observers.removeValue(forKey: token) } // Testing helpers func reset() { storage.removeAll() observers.removeAll() } func hasValue(forKey key: String, domain: String) -> Bool { return storage["\(domain).\(key)"] != nil } } // Use in tests func testSettingsBehavior() { let mockStore = MockSettingsStore() let testSettings = AXPreferenceContext.$store.withValue(mockStore) { AppSettings() } // Test behavior testSettings.userName = "TestUser" XCTAssertTrue(mockStore.hasValue(forKey: "AppSettingsUserName", domain: "com.myapp.settings")) XCTAssertEqual(testSettings.userName, "TestUser") } ``` ### Convenience Methods Some settings classes provide convenience methods for common store scenarios: ```swift @AXSettingsGroup(domain: "com.myapp.settings") class AppSettings { @AXPref var userName: String = "Guest" @AXPref var isFirstLaunch: Bool = true } // Standard instance (uses default store) let settings = AppSettings() // Testing instance (uses in-memory store) let testSettings = AppSettings.testSettings() // Custom store instance let customStoreSettings = AppSettings.make(alternateStore: MyCustomStore()) ``` ## 4. Attributes System (SwiftUI-inspired) ### Understanding AXSettingsAttributes The AXPref system uses a SwiftUI-inspired attributes mechanism that directly mirrors how `EnvironmentValues` work in SwiftUI. This provides extensible metadata for your settings, allowing you to declaratively configure behavior, appearance, and capabilities without cluttering the core setting declaration. ```swift @AXSettingsGroup(domain: "com.myapp.settings") class AppSettings { @AXPref(traits: [.watchSync, .buddyExclude]) var notificationsEnabled: Bool = true @AXPref(traits: [.guestPassInclude]) @AXPrefAttr(\.watchKey, AXPreferenceKeyPair(key: "watch_theme", domain: "com.myapp.watch")) var themePreference: Theme = .automatic } ``` ### Core Attributes #### Traits The `traits` parameter accepts an `AXSettingsTrait` option set that controls various behaviors: ```swift @AXPref(traits: [.watchSync]) var syncToWatch: Bool = false @AXPref(traits: [.buddyExclude]) var localOnlyPreference: String = "device-specific" @AXPref(traits: [.guestPassInclude]) var guestAccessibleSetting: Bool = true @AXPref(traits: [.managedAssets]) var managedResourcePath: String = "" ``` #### Watch Key Mapping Use `@AXPrefAttr(\.watchKey, ...)` to map a setting to a different key/domain on the watch: ```swift @AXPref(traits: [.watchSync]) @AXPrefAttr(\.watchKey, AXPreferenceKeyPair(key: "complications_enabled", domain: "com.myapp.watchface")) var showComplications: Bool = true ``` ### Creating Custom Attributes (EnvironmentValues Pattern) Custom attributes follow the exact same pattern as SwiftUI's `EnvironmentValues`. You create an attribute key type and extend `AXSettingsAttributes` with a computed property: #### Step 1: Define the Attribute Key ```swift // Private key type - follows the same pattern as SwiftUI EnvironmentKeys private struct UISection: AXSettingsAttributeKey { static var defaultValue: UISectionValue? { nil } } private struct ValidationRule: AXSettingsAttributeKey { static var defaultValue: ValidationRuleValue? { nil } } ``` #### Step 2: Define the Attribute Value Types ```swift public struct UISectionValue { public let title: String public let icon: String public init(title: String, icon: String = "gear") { self.title = title self.icon = icon } } public struct ValidationRuleValue { public let minValue: Double public let maxValue: Double public let errorMessage: String public init(min: Double, max: Double, error: String = "Value out of range") { self.minValue = min self.maxValue = max self.errorMessage = error } } ``` #### Step 3: Extend AXSettingsAttributes ```swift extension AXSettingsAttributes { public var uiSection: UISectionValue? { get { self[UISection.self] } set { self[UISection.self] = newValue } } public var validationRule: ValidationRuleValue? { get { self[ValidationRule.self] } set { self[ValidationRule.self] = newValue } } } ``` This is identical to how SwiftUI's `EnvironmentValues` work: ```swift // SwiftUI pattern (for comparison) private struct MyEnvironmentKey: EnvironmentKey { static let defaultValue: String = "default" } extension EnvironmentValues { var myValue: String { get { self[MyEnvironmentKey.self] } set { self[MyEnvironmentKey.self] = newValue } } } ``` ### Built-in Watch Key Example Here's how the built-in `watchKey` attribute is implemented: ```swift // MARK: Watch sync attribute private struct AXWatchPreferenceKey: AXSettingsAttributeKey { static var defaultValue: AXPreferenceKeyPair? { nil } } public struct AXPreferenceKeyPair { public let key: String public let domain: String public init(key: String, domain: String = "com.apple.Accessibility") { self.key = key self.domain = domain } } extension AXSettingsAttributes { public var watchKey: AXPreferenceKeyPair? { get { self[AXWatchPreferenceKey.self] } set { self[AXWatchPreferenceKey.self] = newValue } } } ``` ### Setting Custom Attributes with @AXPrefAttr Once you've defined custom attributes using the EnvironmentValues pattern, you set them using `@AXPrefAttr` with keypath syntax: ```swift @AXSettingsGroup(domain: "com.myapp.settings") class AppSettings { @AXPref(traits: [.watchSync]) @AXPrefAttr(\.uiSection, UISectionValue(title: "Audio Settings", icon: "speaker.wave.3")) @AXPrefAttr(\.validationRule, ValidationRuleValue(min: 0.0, max: 1.0, error: "Volume must be between 0 and 1")) var masterVolume: Double = 0.5 @AXPref(traits: [.guestPassInclude]) @AXPrefAttr(\.uiSection, UISectionValue(title: "Privacy", icon: "lock.shield")) var allowGuestAccess: Bool = false } ``` ### Multiple Attributes on One Setting You can apply multiple `@AXPrefAttr` macros to a single setting: ```swift @AXPref(key: "advanced_logging", traits: [.buddyExclude]) @AXPrefAttr(\.uiSection, UISectionValue(title: "Developer", icon: "hammer")) @AXPrefAttr(\.validationRule, ValidationRuleValue(min: 0.0, max: 10.0)) var debugLoggingLevel: Int = 0 ``` ### Real-World Example Here's how the accessibility system uses custom attributes: ```swift @AXSettingsGroup(domain: "com.apple.accessibility") class VoiceOverSettings { @AXPref(traits: [.watchSync]) @AXPrefAttr(\.watchKey, AXPreferenceKeyPair(key: "vo_speaking_rate")) var speakingRate: Double = 0.5 @AXPref(key: "TripleClickChoice", traits: [.watchSync]) @AXPrefAttr(\.watchKey, AXPreferenceKeyPair(key: "TripleClickOptions")) // Bridge sync uses different key var tripleClickItems: [AXSTripleClickOption] = [] @AXPref(key: "NavigationStyle", traits: [.watchSync, .guestPassInclude]) @AXPrefAttr(\.watchKey, AXPreferenceKeyPair(key: "nav_style_watch")) var navigationStyle: NavigationStyle = .grouped } ``` ### Accessing Attributes at Runtime The attributes system provides runtime introspection capabilities. Thanks to dynamic member lookup, attributes are automatically passed through from the settings record, making access seamless: ```swift let settings = AppSettings() // Access the setting record to examine attributes let volumeSetting = settings.$masterVolume // AXSettingRecord // Check traits (special case attribute) if volumeSetting.traits.contains(.watchSync) { print("This setting syncs to watch") } // Access watch key mapping - dynamic member lookup makes this seamless if let watchKey = volumeSetting.watchKey { print("Watch key: \(watchKey.key) in domain: \(watchKey.domain)") } // Access custom attributes directly through dynamic member lookup if let uiSection = volumeSetting.uiSection { print("Section: \(uiSection.title), Icon: \(uiSection.icon)") } if let validation = volumeSetting.validationRule { print("Valid range: \(validation.minValue) - \(validation.maxValue)") } ``` The dynamic member lookup system automatically forwards attribute access from the `AXSettingRecord` to the underlying `AXSettingsAttributes`, so you can access both built-in attributes like `watchKey` and your custom attributes directly on the setting record without needing to go through an `.attributes` property. This makes the API clean and intuitive - just access the attribute name directly on the setting record, and the system handles the forwarding automatically. ### Why This Pattern? This EnvironmentValues-inspired pattern provides several benefits: - **Familiar to SwiftUI developers**: Uses the same mental model as SwiftUI's environment system - **Type-safe**: Compile-time checking of attribute types and values - **Extensible**: Easy to add new attributes without changing core system - **Performant**: Efficient key-based lookup system - **Composable**: Multiple attributes can be combined on a single setting ## 5. Runtime Querying & Introspection ### Enumerating Settings by Traits The AXPref system provides powerful runtime introspection capabilities, allowing you to query and filter settings based on their traits and attributes. This is particularly useful for creating dynamic UIs, debugging tools, and cross-platform sync logic. ```swift @AXSettingsGroup(domain: "com.myapp.settings") class AppSettings { @AXPref(traits: [.watchSync]) var notificationsEnabled: Bool = true @AXPref(traits: [.watchSync, .guestPassInclude]) var themeMode: ThemeMode = .automatic @AXPref(traits: [.buddyExclude]) var deviceSpecificSetting: String = "local-only" @AXPref(traits: [.managedAssets]) var managedResourcePath: String = "" } ``` ### Querying Settings by Traits ```swift let settings = AppSettings() // Find all settings that sync to watch let watchSyncedSettings = settings.settings(withTraits: [.watchSync]) for setting in watchSyncedSettings { print("Watch synced: \(setting.key) = \(setting.encodedValue ?? "nil")") } // Find settings that are guest accessible let guestSettings = settings.settings(withTraits: [.guestPassInclude]) // Find settings with specific trait combinations let watchAndGuestSettings = settings.settings(withTraits: [.watchSync, .guestPassInclude]) // Find settings that DON'T have certain traits let localOnlySettings = settings.settings(excludingTraits: [.watchSync]) ``` ### Accessing Setting Metadata Each setting record provides comprehensive metadata access: ```swift let settings = AppSettings() // Iterate through all settings in the group for (propertyName, settingRecord) in settings.allSettings { let record = settingRecord as! AXSettingProtocol print("Setting: \(propertyName)") print(" Key: \(record.key)") print(" Domain: \(record.domain)") print(" Traits: \(record.traits)") print(" Current Value: \(record.encodedValue ?? "nil")") // Access custom attributes if present if let watchKey = record.watchKey { print(" Watch Key: \(watchKey.key) in \(watchKey.domain)") } print("---") } ``` ### Settings Groups and Hierarchical Organization Settings groups can be organized hierarchically and queried at different levels: ```swift @AXSettingsGroup(domain: "com.myapp.ui", keyPrefix: "Interface") class UISettings { @AXPref(traits: [.watchSync]) @AXPrefAttr(\.uiSection, UISectionValue(title: "Appearance", icon: "paintbrush")) var themeMode: ThemeMode = .automatic @AXPref(traits: [.guestPassInclude]) @AXPrefAttr(\.uiSection, UISectionValue(title: "Accessibility", icon: "accessibility")) var highContrastMode: Bool = false } @AXSettingsGroup(domain: "com.myapp.data", keyPrefix: "Data") class DataSettings { @AXPref(traits: [.buddyExclude]) @AXPrefAttr(\.uiSection, UISectionValue(title: "Storage", icon: "internaldrive")) var cacheSize: Int = 100_000_000 } ``` When you nest settings groups, queries work accross all nested settings as well. ### Dynamic UI Generation Based on Metadata ```swift struct DynamicSettingsView: View { let settingsManager = AppSettingsManager() var body: some View { NavigationView { List { ForEach(settingsManager.settingsBySection().keys.sorted(), id: \.self) { sectionName in Section(header: sectionHeader(for: sectionName)) { ForEach(settingsManager.settingsBySection()[sectionName] ?? [], id: \.0) { name, setting in settingRow(name: name, setting: setting) } } } } .navigationTitle("Settings") } } @ViewBuilder private func sectionHeader(for sectionName: String) -> some View { // Find the first setting in this section to get icon info if let firstSetting = settingsManager.settingsBySection()[sectionName]?.first?.1, let uiSection = firstSetting.uiSection { Label(sectionName, systemImage: uiSection.icon) } else { Text(sectionName) } } @ViewBuilder private func settingRow(name: String, setting: AXSettingProtocol) -> some View { HStack { Text(name.camelCaseToWords()) Spacer() Text("\(setting.encodedValue?.description ?? "nil")") .foregroundColor(.secondary) } .badge(setting.traits.contains(.watchSync) ? "Watch" : nil) } } extension String { func camelCaseToWords() -> String { return self.replacingOccurrences(of: "([a-z])([A-Z])", with: "$1 $2", options: .regularExpression) } } ``` ## 6. Observability & Streaming ### Swift Concurrency Integration The AXPref system is built from the ground up with Swift's modern concurrency features. Every setting automatically provides reactive streams and integrates seamlessly with Swift's `async`/`await` patterns and the Observation framework. ```swift @AXSettingsGroup(domain: "com.myapp.settings") class AppSettings { @AXPref(traits: [.watchSync]) var notificationsEnabled: Bool = true @AXPref var userTheme: Theme = .automatic @AXPref var maxCacheSize: Int = 100_000_000 } ``` ### AsyncStream Support for Real-time Updates Every setting provides an `AsyncStream` that emits values whenever the setting changes: ```swift let settings = AppSettings() // Listen to a single setting Task { for await enabled in settings.$notificationsEnabled.stream { print("Notifications enabled: \(enabled)") updateNotificationSystem(enabled: enabled) } } // Listen to multiple settings Task { for await theme in settings.$userTheme.stream { print("Theme changed to: \(theme)") updateAppearance(theme: theme) } } ``` ### AXFetchableStream for Current + Future Values The `fetchableStream` provides both the current value and future updates, perfect for UI initialization and reactive programming patterns: ```swift let settings = AppSettings() // Get current value immediately, then listen for changes Task { let stream = settings.$notificationsEnabled.fetchableStream // Get current value by calling the stream as an async function let currentValue = await stream() // Async call updateUI(enabled: currentValue) // Listen for future changes for await newValue in stream { updateUI(enabled: newValue) } } ``` ### Advanced AXFetchableStream Usage `AXFetchableStream` provides powerful transformation and composition methods for complex reactive scenarios: ```swift // Transform values using asyncMap let voiceSelectionStream: AXFetchableStream<[Locale.LanguageCode: VoiceSelection]> = settings.$voiceSelections.fetchableStream // Create a derived stream for a specific language let englishVoiceStream = voiceSelectionStream.asyncMap { selections in await selections.selection(forLanguage: .init("en"), withResolver: VoiceResolver.shared) } // Use the derived stream - async callable function for current value Task { let currentEnglishVoice = await englishVoiceStream() // Async function call print("Current English voice: \(currentEnglishVoice?.voiceId ?? "none")") for await voice in englishVoiceStream { print("English voice changed: \(voice?.voiceId ?? "none")") } } ``` ### SwiftUI Integration Settings work seamlessly with SwiftUI's observation system: ```swift struct SettingsView: View { @Bindable private var settings = AppSettings() var body: some View { Form { Section("Notifications") { Toggle("Enable Notifications", isOn: $settings.notificationsEnabled) } Section("Appearance") { Picker("Theme", selection: settings.$userTheme) { Text("Light").tag(Theme.light) Text("Dark").tag(Theme.dark) Text("Automatic").tag(Theme.automatic) } } Section("Storage") { Stepper("Max Cache Size: \(settings.maxCacheSize.formatted(.byteCount(style: .memory)))", value: settings.$maxCacheSize, in: 10_000_000...1_000_000_000, step: 10_000_000) } } .navigationTitle("Settings") } } ``` ### Caching and Performance Considerations The system includes intelligent caching to optimize performance: ```swift let settings = AppSettings() // First access reads from store and caches let theme1 = settings.userTheme // Store access + cache // Subsequent accesses use cache let theme2 = settings.userTheme // Cache hit let theme3 = settings.userTheme // Cache hit // Cache is invalidated when value changes settings.userTheme = .dark // Cache cleared, observers notified // Next access reads fresh value let theme4 = settings.userTheme // Store access + new cache ``` ### Manual Cache Management For advanced scenarios, you can manually control caching: ```swift let settings = AppSettings() // Purge cache for a specific setting settings.$userTheme.purgeCache() // Next access will read fresh from store let freshTheme = settings.userTheme ``` ## 6. Objective-C Interop ### @objc Declaration Exposure The AXPref system provides seamless interoperability with Objective-C code through standard `@objc` declarations. Settings are exposed to Objective-C by adding `@objc` after the `@AXPref` macro. ```swift @AXSettingsGroup(domain: "com.apple.accessibility") class AccessibilitySettings { @AXPref(traits: [.watchSync]) @objc var voiceOverEnabled: Bool = false @AXPref @objc var speakingRate: Double = 0.5 @AXPref // Not exposed to Objective-C var internalSwiftOnlySetting: String = "swift-only" @AXPref @objc var automaticallyExposed: Bool = true } ``` ### Objective-C Usage Settings marked with `@objc` are automatically available in Objective-C: ```objc AccessibilitySettings *settings = [[AccessibilitySettings alloc] init]; // Read values BOOL voiceOverOn = settings.voiceOverEnabled; double rate = settings.speakingRate; // Write values settings.voiceOverEnabled = YES; settings.speakingRate = 0.75; NSLog(@"VoiceOver: %@, Rate: %f", voiceOverOn ? @"ON" : @"OFF", rate); ``` ### Objective-C Bridgeable Types For Objective-C interop, you can only use types that are bridgeable to Objective-C. Complex Swift-only types cannot be directly exposed: ```swift @AXSettingsGroup(domain: "com.apple.accessibility") class VoiceSettings { // ✅ Objective-C bridgeable types work @AXPref @objc var voiceIdentifier: String = "com.apple.voice.compact.en-US.Samantha" @AXPref @objc var speakingRate: Double = 0.5 @AXPref @objc var isEnabled: Bool = true @AXPref @objc var voiceGender: Int = 0 // Use enum raw values @AXPref @objc var voiceSettings: NSDictionary = [ "rate": 0.5, "pitch": 0.0, "volume": 1.0 ] // ❌ Complex Swift types cannot be directly exposed with @objc @AXPref // No @objc - Swift only var complexSwiftStruct: VoicePreference = VoicePreference.default @AXPref // No @objc - Swift only var advancedConfiguration: [String: CustomStruct] = [:] } ``` ### Legacy Integration with AXSettings_Swift.h For existing Objective-C codebases that need to integrate with the new Swift settings system, you can expose Swift settings through the `AXSettings_Swift.h` header. This approach allows legacy Objective-C code to access Swift settings without requiring changes to the Objective-C implementation. #### Adding to AXSettings_Swift.h The `AXSettings_Swift.h` header acts as a bridge between your Swift settings and legacy Objective-C code. You add property declarations that forward to your Swift settings there. Objective c forwarding and reflection are used to forward the selector to the correct setting group.