Add @Publisher behaviour for computed property

♀尐吖头ヾ 提交于 2019-12-30 12:40:48

问题


I am trying to make a ObservableObject that has properties that wrap a UserDefaults variable.

In order to conform to ObservableObject, I need to wrap the properties with @Published. Unfortunately, I cannot apply that to computed properties, as I use for the UserDefaults values.

How could I make it work? What do I have to do to achieve @Published behaviour?


回答1:


For an existing @Published property

Here's one way to do it, you can create a lazy property that returns a publisher derived from your @Published publisher:

import Combine

class AppState: ObservableObject {
  @Published var count: Int = 0
  lazy var countTimesTwo: AnyPublisher<Int, Never> = {
    $count.map { $0 * 2 }.eraseToAnyPublisher()
  }()
}

let appState = AppState()
appState.count += 1
appState.$count.sink { print($0) }
appState.countTimesTwo.sink { print($0) }
// => 1
// => 2
appState.count += 1
// => 2
// => 4

However, this is contrived and probably has little practical use. See the next section for something more useful...


For any object that supports KVO

UserDefaults supports KVO. We can create a generalizable solution called KeyPathObserver that reacts to changes to an Object that supports KVO with a single @ObjectObserver. The following example will run in a Playground:

import Foundation
import UIKit
import PlaygroundSupport
import SwiftUI
import Combine

let defaults = UserDefaults.standard

extension UserDefaults {
  @objc var myCount: Int {
    return integer(forKey: "myCount")
  }

  var myCountSquared: Int {
    return myCount * myCount
  }
}

class KeyPathObserver<T: NSObject, V>: ObservableObject {
  @Published var value: V
  private var cancel = Set<AnyCancellable>()

  init(_ keyPath: KeyPath<T, V>, on object: T) {
    value = object[keyPath: keyPath]
    object.publisher(for: keyPath)
      .assign(to: \.value, on: self)
      .store(in: &cancel)
  }
}

struct ContentView: View {
  @ObservedObject var defaultsObserver = KeyPathObserver(\.myCount, on: defaults)

  var body: some View {
    VStack {
      Text("myCount: \(defaults.myCount)")
      Text("myCountSquared: \(defaults.myCountSquared)")
      Button(action: {
        defaults.set(defaults.myCount + 1, forKey: "myCount")
      }) {
        Text("Increment")
      }
    }
  }
}
let viewController = UIHostingController(rootView: ContentView())
PlaygroundPage.current.liveView = viewController

note that we've added an additional property myCountSquared to the UserDefaults extension to calculate a derived value, but observe the original KeyPath.




回答2:


When Swift is updated to enable nested property wrappers, the way to do this will probably be to create a @UserDefault property wrapper and combine it with @Published.

In the mean time, I think the best way to handle this situation is to implement ObservableObject manually instead of relying on @Published. Something like this:

class ViewModel: ObservableObject {
    let objectWillChange = ObservableObjectPublisher()

    var name: String {
        get {
            UserDefaults.standard.string(forKey: "name") ?? ""
        }
        set {
            objectWillChange.send()
            UserDefaults.standard.set(newValue, forKey: "name")
        }
    }
}

Property wrapper

As I mentioned in the comments, I don't think there is a way to wrap this up in a property wrapper that removes all boilerplate, but this is the best I can come up with:

@propertyWrapper
struct PublishedUserDefault<T> {
    private let key: String
    private let defaultValue: T

    var objectWillChange: ObservableObjectPublisher?

    init(wrappedValue value: T, key: String) {
        self.key = key
        self.defaultValue = value
    }

    var wrappedValue: T {
        get {
            UserDefaults.standard.object(forKey: key) as? T ?? defaultValue
        }
        set {
            objectWillChange?.send()
            UserDefaults.standard.set(newValue, forKey: key)
        }
    }
}

class ViewModel: ObservableObject {
    let objectWillChange = ObservableObjectPublisher()

    @PublishedUserDefault(key: "name")
    var name: String = "John"

    init() {
        _name.objectWillChange = objectWillChange
    }
}

You still need to declare objectWillChange and connect it to your property wrapper somehow (I'm doing it in init), but at least the property definition itself it pretty simple.




回答3:


Updated: I've managed to add support for the $variable syntax. However, for the enclosing ObservableObject to propagate changes, I manually observe the UserDefaults themselves:

class Settings: ObservableObject {
  let objectWillChange = PassthroughSubject<Void, Never>()
  private var didChangeCancellable: AnyCancellable?
  private init(){
    didChangeCancellable = NotificationCenter.default
      .publisher(for: UserDefaults.didChangeNotification)
      .map { _ in () }
      .receive(on: DispatchQueue.main)
      .subscribe(objectWillChange)
  }

  static var shared = Settings()
  @Setting(key: "test") var isTest = false
}

@propertyWrapper
public struct Setting<T> {

  let key: String
  let defaultValue: T

  init(wrappedValue value: T, key: String) {
    self.key = key
    self.defaultValue = value
  }

  public var wrappedValue: T {
    get {
      let val = UserDefaults.standard.object(forKey: key) as? T
      return val ?? defaultValue
    }
    set {
      objectWillChange?.send()
      publisher?.subject.value = newValue
      UserDefaults.standard.set(newValue, forKey: key)
    }
  }

  /// NEW
  public struct Publisher: Combine.Publisher {

    public typealias Output = T

    public typealias Failure = Never

    public func receive<Downstream: Subscriber>(subscriber: Downstream)
      where Downstream.Input == T, Downstream.Failure == Never {
        subject.subscribe(subscriber)
    }

    fileprivate let subject: Combine.CurrentValueSubject<T, Never>

    fileprivate init(_ output: Output) {
      subject = .init(output)
    }
  }

  private var publisher: Publisher?

  internal var objectWillChange: ObservableObjectPublisher?

  public var projectedValue: Publisher {
    mutating get {
      if let publisher = publisher {
        return publisher
      }
      let publisher = Publisher(wrappedValue)
      self.publisher = publisher
      return publisher
    }
  }
}



来源:https://stackoverflow.com/questions/59036863/add-publisher-behaviour-for-computed-property

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!