I have a first @property wrapper that is made to be UserDefaults called @UserDefault(key: .isSignedIn, defaultValue: false) var isSignedIn: Bool
. it is working fine as a publisher by using it this way, $isSignedIn
to update my SwiftUI view when used by itself. But I would like to make it in a dependency set in a view model.
Now, to hold UserDefaults values, I create a dependency set as a @propertyWrapper called @Injection(\.localDataManager) var localDataManager
that will also be used in unit tests using a subscript solution associated with key called InjectedDependency[\.localDataManager] = LocalDataManagerMock()
The problem is that LocalDataManager
class is not a @ObservableObject
and does not update my SwiftUI view over time.
- How can
LocalDataManager
be an@ObservableObject
using thisInjectedDependency
unit testing solution?
The SwiftUI view with the view model:
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
}
What I would like to use as a @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
}
}
The @UserDefault
code:
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()
}
}
The InjectedDependency
solution for Unit Testing:
// 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()
}