【问题标题】:How to use Combine framework NSObject.KeyValueObservingPublisher?如何使用组合框架 NSObject.KeyValueObservingPublisher?
【发布时间】:2020-02-25 00:40:50
【问题描述】:

我正在尝试使用组合框架 NSObject.KeyValueObservingPublisher。我可以通过在 NSObject 上调用 publisher(for:options:) 来了解如何生成此发布者。但我有两个问题:

  • 我可以在options 中包含.old,但没有.old 值到达。唯一出现的值是.initial 值(当我们订阅时)和.new 值(每次观察到的属性发生变化时)。我可以抑制 .initial 值,但不能抑制 .new 值或添加 .old 值。

  • 如果options[.initial, .new](默认值),我看不出有什么方法可以区分我收到的值是.initial 还是.new。使用“真实”KVO,我得到一个 NSKeyValueChangeKey 或一个 NSKeyValueObservedChange,它告诉我我得到了什么。但是对于联合出版商,我不知道。我只是得到未标记的值。

在我看来,这些限制使这个发布者几乎无法使用,除非在最简单的情况下。有什么解决方法吗?

【问题讨论】:

  • 问题是为什么需要旧值?发布者的一个点是为了新的价值。如果一个新的发布者管道需要一个旧值,它可以从像 CurrentValueSubject 这样的主题中获取它。
  • 虽然是一个不同的问题,但这也可能有所帮助:stackoverflow.com/q/65965666

标签: ios combine


【解决方案1】:

我对 TylerTheCompiler 的回答没有什么要补充的,但我想说明几点:

  1. 根据我的测试,NSObject.KeyValueObservingPublisher 在内部不使用更改字典。它总是使用键路径来获取属性的值。

  2. 如果您传递.prior,则发布者将在每次属性更改时分别发布之前和之后的值。

  3. 获取属性前后值的更短方法是使用scan 运算符:

    extension Publisher {
        func withPriorValue() -> AnyPublisher<(prior: Output?, new: Output), Failure> {
            return self
                .scan((prior: Output?.none, new: Output?.none)) { (prior: $0.new, new: $1) }
                .map { (prior: $0.0, new: $0.1!) }
                .eraseToAnyPublisher()
        }
    }
    

    如果您还使用.initial,那么withPriorValue 的第一个输出将是(prior: nil, new: currentValue)

【讨论】:

  • 啊哈,scan。我看到了,但我不确定如何使用它,或者它是否是我正在寻找的东西。好的!老实说,这可能应该是公认的答案,因为它比我的要简洁得多。
【解决方案2】:

为了获取旧值,我能找到的唯一解决方法是使用.prior 而不是.old,这会导致发布者在之前发出属性的当前值已更改,然后使用 collect(2) 将该值与下一个发射(即属性的新值)结合起来。

为了确定什么是初始值和新值,我发现的唯一解决方法是在发布者上使用 first()

然后,我合并了这两个发布者,并将它们全部封装在一个漂亮的小函数中,该函数会生成一个自定义的 KeyValueObservation 枚举,让您可以轻松确定它是否是初始值,如果是,还会为您提供旧值不是初始值。

完整的示例代码如下。只需在 Xcode 中创建一个全新的单视图项目,并将 ViewController.swift 的内容替换为以下所有内容:

import UIKit
import Combine

/// The type of value published from a publisher created from 
/// `NSObject.keyValueObservationPublisher(for:)`. Represents either an
/// initial KVO observation or a non-initial KVO observation.
enum KeyValueObservation<T> {
    case initial(T)
    case notInitial(old: T, new: T)

    /// Sets self to `.initial` if there is exactly one element in the array.
    /// Sets self to `.notInitial` if there are two or more elements in the array.
    /// Otherwise, the initializer fails.
    ///
    /// - Parameter values: An array of values to initialize with.
    init?(_ values: [T]) {
        if values.count == 1, let value = values.first {
            self = .initial(value)
        } else if let old = values.first, let new = values.last {
            self = .notInitial(old: old, new: new)
        } else {
            return nil
        }
    }
}

extension NSObjectProtocol where Self: NSObject {

    /// Publishes `KeyValueObservation` values when the value identified 
    /// by a KVO-compliant keypath changes.
    ///
    /// - Parameter keyPath: The keypath of the property to publish.
    /// - Returns: A publisher that emits `KeyValueObservation` elements each 
    ///            time the property’s value changes.
    func keyValueObservationPublisher<Value>(for keyPath: KeyPath<Self, Value>)
        -> AnyPublisher<KeyValueObservation<Value>, Never> {

        // Gets a built-in KVO publisher for the property at `keyPath`.
        //
        // We specify all the options here so that we get the most information
        // from the observation as possible.
        //
        // We especially need `.prior`, which makes it so the publisher fires 
        // the previous value right before any new value is set to the property.
        //
        // `.old` doesn't seem to make any difference, but I'm including it
        // here anyway for no particular reason.
        let kvoPublisher = publisher(for: keyPath,
                                     options: [.initial, .new, .old, .prior])

        // Makes a publisher for just the initial value of the property.
        //
        // Since we specified `.initial` above, the first published value will
        // always be the initial value, so we use `first()`.
        //
        // We then map this value to a `KeyValueObservation`, which in this case
        // is `KeyValueObservation.initial` (see the initializer of
        // `KeyValueObservation` for why).
        let publisherOfInitialValue = kvoPublisher
            .first()
            .compactMap { KeyValueObservation([$0]) }

        // Makes a publisher for every non-initial value of the property.
        //
        // Since we specified `.initial` above, the first published value will 
        // always be the initial value, so we ignore that value using 
        // `dropFirst()`.
        //
        // Then, after the first value is ignored, we wait to collect two values
        // so that we have an "old" and a "new" value for our 
        // `KeyValueObservation`. This works because we specified `.prior` above, 
        // which causes the publisher to emit the value of the property
        // _right before_ it is set to a new value. This value becomes our "old"
        // value, and the next value emitted becomes the "new" value.
        // The `collect(2)` function puts the old and new values into an array, 
        // with the old value being the first value and the new value being the 
        // second value.
        //
        // We then map this array to a `KeyValueObservation`, which in this case 
        // is `KeyValueObservation.notInitial` (see the initializer of 
        // `KeyValueObservation` for why).
        let publisherOfTheRestOfTheValues = kvoPublisher
            .dropFirst()
            .collect(2)
            .compactMap { KeyValueObservation($0) }

        // Finally, merge the two publishers we created above
        // and erase to `AnyPublisher`.
        return publisherOfInitialValue
            .merge(with: publisherOfTheRestOfTheValues)
            .eraseToAnyPublisher()
    }
}

class ViewController: UIViewController {

    /// The property we want to observe using our KVO publisher.
    ///
    /// Note that we need to make this visible to Objective-C with `@objc` and 
    /// to make it work with KVO using `dynamic`, which means the type of this 
    /// property must be representable in Objective-C. This one works because it's 
    /// a `String`, which has an Objective-C counterpart, `NSString *`.
    @objc dynamic private var myProperty: String?

    /// The thing we have to hold on to to cancel any further publications of any
    /// changes to the above property when using something like `sink`, as shown
    /// below in `viewDidLoad`.
    private var cancelToken: AnyCancellable?

    override func viewDidLoad() {
        super.viewDidLoad()

        // Before this call to `sink` even finishes, the closure is executed with
        // a value of `KeyValueObservation.initial`.
        // This prints: `Initial value of myProperty: nil` to the console.
        cancelToken = keyValueObservationPublisher(for: \.myProperty).sink { 
            switch $0 {
            case .initial(let value):
                print("Initial value of myProperty: \(value?.quoted ?? "nil")")

            case .notInitial(let oldValue, let newValue):
                let oldString = oldValue?.quoted ?? "nil"
                let newString = newValue?.quoted ?? "nil"
                print("myProperty did change from \(oldString) to \(newString)")
            }
        }

        // This prints:
        // `myProperty did change from nil to "First value"`
        myProperty = "First value"

        // This prints:
        // `myProperty did change from "First value" to "Second value"`
        myProperty = "Second value"

        // This prints:
        // `myProperty did change from "Second value" to "Third value"`
        myProperty = "Third value"

        // This prints:
        // `myProperty did change from "Third value" to nil`
        myProperty = nil
    }
}

extension String {

    /// Ignore this. This is just used to make the example output above prettier.
    var quoted: String { "\"\(self)\"" }
}

【讨论】:

  • 基本上,您正在构建我所期望的 Combine 发布者一开始就应该做的事情!很好,谢谢。 — 这就像奇怪地缺少 UIControl 发布者。是的,你可以建造一个,是的,我已经做到了,但这是多么奇怪的遗漏;它让整个框架感觉不成熟。
  • 没有问题。是的,同意——在我看来,这就是联合出版商应该出版的。
猜你喜欢
  • 2021-02-27
  • 2016-03-26
  • 2020-04-08
  • 1970-01-01
  • 1970-01-01
  • 2017-04-24
  • 2014-07-20
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多