如何将依赖项设置为 @propertyWrapper 是 @ObservableObject?

How to have a dependency set as a @propertyWrapper be a @ObservableObject?

我有一个名为 @UserDefault(key: .isSignedIn, defaultValue: false) var isSignedIn: Bool 的第一个 @属性 包装器,它是 UserDefaults。通过这种方式使用它作为发布者工作正常,$isSignedIn 在单独使用时更新我的​​ SwiftUI 视图。但是我想把它放在视图模型的依赖项中。

现在,为了保存 UserDefaults 值,我创建了一个名为 @Injection(\.localDataManager) var localDataManager 的依赖集作为 @属性Wrapper,它也将用于单元测试,使用与名为 [=] 的键关联的下标解决方案17=]

问题是 LocalDataManager class 不是 @ObservableObject 并且不会随时间更新我的 SwiftUI 视图。

带有视图模型的 SwiftUI 视图:

import SwiftUI

struct ContentView: View {

  @StateObject var viewModel = DataViewModel()

  var body: some View {
    VStack(spacing: 16) {
      Text(viewModel.localDataManager.isSignedIn.description)

      Button(action: { viewModel.localDataManager.isSignedIn.toggle() }) {
        Text("Toggle boolean button")
      }
    }
  }
}

class DataViewModel: ObservableObject {

  @Injection(\.localDataManager) var localDataManager
}

我想用作 @ObservableObject:

import Foundation

protocol LocalDataManagerProtocol {
  var isSignedIn: Bool { get set }
}

final class LocalDataManager: LocalDataManagerProtocol {

  @UserDefault(key: .isSignedIn, defaultValue: false)
  var isSignedIn: Bool
}

@propertyWrapper
struct Injection<T> {

  private let keyPath: WritableKeyPath<InjectedDependency, T>

  var wrappedValue: T {
    get { InjectedDependency[keyPath] }
    set { InjectedDependency[keyPath] = newValue }
  }

  init(_ keyPath: WritableKeyPath<InjectedDependency, T>) {
    self.keyPath = keyPath
  }
}

@UserDefault代码:

import Combine
import SwiftUI

enum UserDefaultsKey: String {
  case isSignedIn
}

protocol UserDefaultsProtocol {
  func object(forKey defaultName: String) -> Any?
  func set(_ value: Any?, forKey defaultName: String)
}
extension UserDefaults: UserDefaultsProtocol {}

@propertyWrapper
struct UserDefault<Value> {

  let key: UserDefaultsKey
  let defaultValue: Value
  var container: UserDefaultsProtocol = UserDefaults.standard

  private let publisher = PassthroughSubject<Value, Never>()

  var wrappedValue: Value {
    get {
      return container.object(forKey: key.rawValue) as? Value ?? defaultValue
    }
    set {
      container.set(newValue, forKey: key.rawValue)
      publisher.send(newValue)
    }
  }

  var projectedValue: AnyPublisher<Value, Never> {
    publisher.eraseToAnyPublisher()
  }
}

单元测试的InjectedDependency解决方案:

// Use in Unit Test code in SetUp()
// `InjectedDependency[\.localDataManager] = LocalDataManagerMock()`

protocol InjectedKeyProtocol {
  associatedtype Value
  static var currentValue: Self.Value { get set }
}

struct InjectedDependency {

private static var current = InjectedDependency()

static subscript<K>(key: K.Type) -> K.Value where K: InjectedKeyProtocol {
    get { key.currentValue }
    set { key.currentValue = newValue }
  }

  static subscript<T>(_ keyPath: WritableKeyPath<InjectedDependency, T>) -> T {
    get { current[keyPath: keyPath] }
    set { current[keyPath: keyPath] = newValue }
  }

  var localDataManager: LocalDataManagerProtocol {
    get { Self[LocalDataManagerKey.self] }
    set { Self[LocalDataManagerKey.self] = newValue }
  }
}

private struct LocalDataManagerKey: InjectedKeyProtocol {
  static var currentValue: LocalDataManagerProtocol = LocalDataManager()
}

您需要让LocalDataManager符合ObservableObject,然后手动将更改后的值下发。

注意新的 willSet 部分:

class DataViewModel: ObservableObject {
    @Injection(\.localDataManager) var localDataManager {
        willSet {
            objectWillChange.send()
        }
    }
}
final class LocalDataManager: ObservableObject, LocalDataManagerProtocol {
    @UserDefault(key: .isSignedIn, defaultValue: false)
    var isSignedIn: Bool {
        willSet {
            objectWillChange.send()
        }
    }
}

结果: