0

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

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

问题是LocalDataManager该类不是一个@ObservableObject并且不会随着时间的推移更新我的 SwiftUI 视图。

  • 如何LocalDataManager使用@ObservableObject这个InjectedDependency单元测试解决方案?

带有视图模型的 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()
}
4

1 回答 1

1

您需要符合LocalDataManagerObservableObject然后手动将更改的值向下发送。

注意新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()
        }
    }
}

结果:

于 2021-09-02T20:57:01.810 回答