如何将依赖项设置为 @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 视图。
- 如何使用此
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()
}
您需要让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()
}
}
}
结果:
我有一个名为 @UserDefault(key: .isSignedIn, defaultValue: false) var isSignedIn: Bool
的第一个 @属性 包装器,它是 UserDefaults。通过这种方式使用它作为发布者工作正常,$isSignedIn
在单独使用时更新我的 SwiftUI 视图。但是我想把它放在视图模型的依赖项中。
现在,为了保存 UserDefaults 值,我创建了一个名为 @Injection(\.localDataManager) var localDataManager
的依赖集作为 @属性Wrapper,它也将用于单元测试,使用与名为 [=] 的键关联的下标解决方案17=]
问题是 LocalDataManager
class 不是 @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()
}
您需要让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()
}
}
}
结果: