import Foundation // Note: Not thread-safe without Mutex, but fine for our single-threaded setup app var appStorageCache: [String: any Codable & Sendable] = [:] private var appStoragePublisherCache: [String: Publisher] = [:] /// Like ``State``, but persists its value to disk so that it survives betweeen /// app launches. @propertyWrapper public struct AppStorage: ObservableProperty { // TODO: Observe changes to persisted values made by external processes private final class Storage: StateStorageProtocol { let mode: Mode var downstreamObservation: Cancellable? var provider: (any AppStorageProvider)? init(mode: Mode, provider: (any AppStorageProvider)?) { self.mode = mode self.provider = provider } lazy var didChange: Publisher = { let cacheKey = mode.pathDescription guard let publisher = appStoragePublisherCache[cacheKey] else { let newPublisher = Publisher() appStoragePublisherCache[cacheKey] = newPublisher return newPublisher } return publisher }() var value: Value { get { switch mode { case .key(let key, let defaultValue): guard let provider else { // NB: We used to call `fatalError` here, but since `StateImpl` // accesses this property on initialization, we're returning the // default value instead. return defaultValue } return provider.getValue(key: key, defaultValue: defaultValue) case .path(let keyPath): return AppStorageValues(provider: provider)[keyPath: keyPath] } } set { guard let provider else { fatalError( """ @AppStorage value with key '\(mode.pathDescription)' used before \ initialization. Don't use @AppStorage properties before \ SwiftCrossUI requests the body of the enclosing 'App' or 'View'. """ ) } switch mode { case .key(let key, _): provider.setValue(key: key, newValue: newValue) case .path(let keyPath): var values = AppStorageValues(provider: provider) values[keyPath: keyPath] = newValue } } } } private let implementation: StateImpl private var storage: Storage { implementation.storage } public var didChange: Publisher { storage.didChange } public var wrappedValue: Value { get { implementation.wrappedValue } nonmutating set { implementation.wrappedValue = newValue } } public var projectedValue: Binding { implementation.projectedValue } public init( wrappedValue defaultValue: Value, _ key: String, provider: (any AppStorageProvider)? = nil ) { implementation = StateImpl( initialStorage: Storage(mode: .key(key, defaultValue), provider: provider) ) } public init( _ key: String, provider: (any AppStorageProvider)? = nil ) where Value: ExpressibleByNilLiteral { self.init(wrappedValue: nil, key, provider: provider) } public func update(with environment: EnvironmentValues, previousValue: AppStorage?) { implementation.update(with: environment, previousValue: previousValue?.implementation) // don't override a provider specified by the initializer if storage.provider == nil { storage.provider = environment.appStorageProvider } } enum Mode { case key(String, Value) case path(WritableKeyPath) var pathDescription: String { switch self { case .key(let key, _): key case .path(let keyPath): "\(keyPath)" } } } } extension AppStorage { @available( *, deprecated, message: "'AppStorage' does not work correctly with classes; use a struct instead" ) public init( wrappedValue defaultValue: Value, _ key: String, provider: (any AppStorageProvider)? = nil ) where Value: AnyObject { implementation = StateImpl( initialStorage: Storage(mode: .key(key, defaultValue), provider: provider) ) } @available( *, deprecated, message: """ 'AppStorage' currently does not persist 'ObservableObject' types \ to disk when published properties update """ ) public init( wrappedValue defaultValue: Value, _ key: String, provider: (any AppStorageProvider)? = nil ) where Value: ObservableObject { implementation = StateImpl( initialStorage: Storage(mode: .key(key, defaultValue), provider: provider) ) } } // MARK: - AppStorageKey extension AppStorage { public init>( _: Key.Type, provider: (any AppStorageProvider)? = nil ) { self.init(wrappedValue: Key.defaultValue, Key.name, provider: provider) } public init( _ keyPath: WritableKeyPath, provider: (any AppStorageProvider)? = nil ) { implementation = StateImpl( initialStorage: Storage(mode: .path(keyPath), provider: provider) ) } }