Tometoyou
Tometoyou

Reputation: 8376

How to create a publishing property wrapper that also caches

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

Answers (1)

Asperi
Asperi

Reputation: 257819

We need a dynamic property in SwiftUI view to make view updated. Here is possible approach based on aggregated State

demo

@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

Related Questions