| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177 |
- 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<Value: Codable & Sendable>: 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<Storage>
- 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<Value> { 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<Value>?) {
- 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<AppStorageValues, Value>)
- 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: AppStorageKey<Value>>(
- _: Key.Type,
- provider: (any AppStorageProvider)? = nil
- ) {
- self.init(wrappedValue: Key.defaultValue, Key.name, provider: provider)
- }
- public init(
- _ keyPath: WritableKeyPath<AppStorageValues, Value>,
- provider: (any AppStorageProvider)? = nil
- ) {
- implementation = StateImpl(
- initialStorage: Storage(mode: .path(keyPath), provider: provider)
- )
- }
- }
|