【问题标题】:How to have a dependency set as a @propertyWrapper be a @ObservableObject?如何将依赖项设置为@propertyWrapper 是@ObservableObject?
【发布时间】:2021-09-01 21:56:26
【问题描述】:

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

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

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

  • 如何使用InjectedDependency 单元测试解决方案使LocalDataManager 成为@ObservableObject

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

【问题讨论】:

  • 你能用这个代码创建一个minimal reproducible example,这样我们就可以将它直接复制粘贴到项目中并找到解决方案吗?只需一个简单的ContentView 显示示例就很完美了,然后您将能够得到一些有用的答案(而不是猜测)
  • 用最小的可重现示例和可以展示我的问题的 ContentView 更新了代码。

标签: swiftui subscript observableobject property-wrapper


【解决方案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()
        }
    }
}

结果:

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2016-03-05
    • 2011-09-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-02-16
    • 2011-10-20
    • 2019-07-10
    相关资源
    最近更新 更多