AppStorage.swift 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
  1. import Foundation
  2. // Note: Not thread-safe without Mutex, but fine for our single-threaded setup app
  3. var appStorageCache: [String: any Codable & Sendable] = [:]
  4. private var appStoragePublisherCache: [String: Publisher] = [:]
  5. /// Like ``State``, but persists its value to disk so that it survives betweeen
  6. /// app launches.
  7. @propertyWrapper
  8. public struct AppStorage<Value: Codable & Sendable>: ObservableProperty {
  9. // TODO: Observe changes to persisted values made by external processes
  10. private final class Storage: StateStorageProtocol {
  11. let mode: Mode
  12. var downstreamObservation: Cancellable?
  13. var provider: (any AppStorageProvider)?
  14. init(mode: Mode, provider: (any AppStorageProvider)?) {
  15. self.mode = mode
  16. self.provider = provider
  17. }
  18. lazy var didChange: Publisher = {
  19. let cacheKey = mode.pathDescription
  20. guard let publisher = appStoragePublisherCache[cacheKey] else {
  21. let newPublisher = Publisher()
  22. appStoragePublisherCache[cacheKey] = newPublisher
  23. return newPublisher
  24. }
  25. return publisher
  26. }()
  27. var value: Value {
  28. get {
  29. switch mode {
  30. case .key(let key, let defaultValue):
  31. guard let provider else {
  32. // NB: We used to call `fatalError` here, but since `StateImpl`
  33. // accesses this property on initialization, we're returning the
  34. // default value instead.
  35. return defaultValue
  36. }
  37. return provider.getValue(key: key, defaultValue: defaultValue)
  38. case .path(let keyPath):
  39. return AppStorageValues(provider: provider)[keyPath: keyPath]
  40. }
  41. }
  42. set {
  43. guard let provider else {
  44. fatalError(
  45. """
  46. @AppStorage value with key '\(mode.pathDescription)' used before \
  47. initialization. Don't use @AppStorage properties before \
  48. SwiftCrossUI requests the body of the enclosing 'App' or 'View'.
  49. """
  50. )
  51. }
  52. switch mode {
  53. case .key(let key, _):
  54. provider.setValue(key: key, newValue: newValue)
  55. case .path(let keyPath):
  56. var values = AppStorageValues(provider: provider)
  57. values[keyPath: keyPath] = newValue
  58. }
  59. }
  60. }
  61. }
  62. private let implementation: StateImpl<Storage>
  63. private var storage: Storage { implementation.storage }
  64. public var didChange: Publisher { storage.didChange }
  65. public var wrappedValue: Value {
  66. get { implementation.wrappedValue }
  67. nonmutating set { implementation.wrappedValue = newValue }
  68. }
  69. public var projectedValue: Binding<Value> { implementation.projectedValue }
  70. public init(
  71. wrappedValue defaultValue: Value,
  72. _ key: String,
  73. provider: (any AppStorageProvider)? = nil
  74. ) {
  75. implementation = StateImpl(
  76. initialStorage: Storage(mode: .key(key, defaultValue), provider: provider)
  77. )
  78. }
  79. public init(
  80. _ key: String,
  81. provider: (any AppStorageProvider)? = nil
  82. ) where Value: ExpressibleByNilLiteral {
  83. self.init(wrappedValue: nil, key, provider: provider)
  84. }
  85. public func update(with environment: EnvironmentValues, previousValue: AppStorage<Value>?) {
  86. implementation.update(with: environment, previousValue: previousValue?.implementation)
  87. // don't override a provider specified by the initializer
  88. if storage.provider == nil {
  89. storage.provider = environment.appStorageProvider
  90. }
  91. }
  92. enum Mode {
  93. case key(String, Value)
  94. case path(WritableKeyPath<AppStorageValues, Value>)
  95. var pathDescription: String {
  96. switch self {
  97. case .key(let key, _):
  98. key
  99. case .path(let keyPath):
  100. "\(keyPath)"
  101. }
  102. }
  103. }
  104. }
  105. extension AppStorage {
  106. @available(
  107. *,
  108. deprecated,
  109. message: "'AppStorage' does not work correctly with classes; use a struct instead"
  110. )
  111. public init(
  112. wrappedValue defaultValue: Value,
  113. _ key: String,
  114. provider: (any AppStorageProvider)? = nil
  115. ) where Value: AnyObject {
  116. implementation = StateImpl(
  117. initialStorage: Storage(mode: .key(key, defaultValue), provider: provider)
  118. )
  119. }
  120. @available(
  121. *,
  122. deprecated,
  123. message: """
  124. 'AppStorage' currently does not persist 'ObservableObject' types \
  125. to disk when published properties update
  126. """
  127. )
  128. public init(
  129. wrappedValue defaultValue: Value,
  130. _ key: String,
  131. provider: (any AppStorageProvider)? = nil
  132. ) where Value: ObservableObject {
  133. implementation = StateImpl(
  134. initialStorage: Storage(mode: .key(key, defaultValue), provider: provider)
  135. )
  136. }
  137. }
  138. // MARK: - AppStorageKey
  139. extension AppStorage {
  140. public init<Key: AppStorageKey<Value>>(
  141. _: Key.Type,
  142. provider: (any AppStorageProvider)? = nil
  143. ) {
  144. self.init(wrappedValue: Key.defaultValue, Key.name, provider: provider)
  145. }
  146. public init(
  147. _ keyPath: WritableKeyPath<AppStorageValues, Value>,
  148. provider: (any AppStorageProvider)? = nil
  149. ) {
  150. implementation = StateImpl(
  151. initialStorage: Storage(mode: .path(keyPath), provider: provider)
  152. )
  153. }
  154. }