Reputation: 8376
I have a caching system that works. I have a property wrapper. Now I want to turn that into a property wrapper that publishes for use in a SwiftUI
view but I can't work that out.
Here is my code for the property wrapper (that doesn't successfully publish). This was sort of taken from another answer on stack overflow, where I've defined an internal CurrentValueSubject
that gets fired every time a new value is set to the wrappedValue
:
import UIKit
import Combine
@propertyWrapper final class CachedPublisher<Value: Codable> {
enum Key: String, Codable {
case user
}
let key: Key
let cache: Cache<Key, Value>
private let defaultValue: Value
private var cancellables = Set<AnyCancellable>()
private lazy var subject = CurrentValueSubject<Value, Error>(wrappedValue)
var wrappedValue: Value {
get {
let value = cache[key]
return value ?? defaultValue
}
set {
cache[key] = newValue
subject.send(newValue)
}
}
var projectedValue: AnyPublisher<Value, Error> {
return subject.eraseToAnyPublisher()
}
init(
wrappedValue defaultValue: Value,
key: Key
) {
self.key = key
self.defaultValue = defaultValue
if let cache = try? Cache<Key, Value>.retrieveFromDisk(withName: key.rawValue) {
self.cache = cache
} else {
cache = Cache<Key, Value>()
}
}
}
// MARK: - ExpressibleByNilLiteral
extension CachedPublisher where Value: ExpressibleByNilLiteral {
convenience init(key: Key) {
self.init(wrappedValue: nil, key: key)
}
}
This is to be specified like this inside an ObservableObject
:
@CachedPublisher(key: .user)
var user: User?
Then I want to use it in a SwiftUI
view so my view updates whenever user
changes.
Update in response to Asperi:
I have an @EnvironmentObject that is my source of truth:
final class MyTruthObject: ObservableObject {
@CachedPublisher(key: .user)
var user: User? = User(name: "Unknown")
}
struct DemoView: View {
@EnvironmentObject private var myTruth: MyTruthObject
var body: some View {
VStack {
Text("User: \(myTruth.user?.name ?? "")")
Divider()
Button("Update") {
myTruth.user = User(name: "John Smith")
}
Button("Reset") {
myTruth.user = nil
}
}
}
}
Would your answer work with this layout?
Upvotes: 2
Views: 844
Reputation: 257819
We need a dynamic property in SwiftUI view to make view updated. Here is possible approach based on aggregated State
@propertyWrapper struct CachedPublisher<Value: Codable>: DynamicProperty {
enum Key: String, Codable {
case user
}
let key: Key
let cache: Cache<Key, Value>
private let defaultValue: Value?
let storage: State<Value?>
var wrappedValue: Value? {
get {
storage.wrappedValue
}
nonmutating set {
let value = newValue ?? defaultValue
cache[key] = value
storage.wrappedValue = value
}
}
var projectedValue: Binding<Value?> {
storage.projectedValue
}
init(
wrappedValue defaultValue: Value?,
key: Key
) {
self.key = key
self.defaultValue = defaultValue
if let cache = try? Cache<Key, Value>.retrieveFromDisk(withName: key.rawValue) {
self.cache = cache
} else {
cache = Cache<Key, Value>()
}
self.storage = State(initialValue: cache[key] ?? defaultValue)
}
}
and tested with Xcode 13.2 / iOS 15.2 (+ some replication for missed components) using
struct DemoView: View {
@CachedPublisher(key: .user)
var user: User? = User(name: "Unknown")
var body: some View {
VStack {
Text("User: \(user?.name ?? "")")
Divider()
Button("Update") {
user = User(name: "John Smith")
}
Button("Reset") {
user = nil
}
}
}
}
Upvotes: 3