本文介绍了如何使用合并框架NSObject.KeyValueObservingPublisher?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

I'm trying to use the Combine framework NSObject.KeyValueObservingPublisher. I can see how to produce this publisher by calling publisher(for:options:) on an NSObject. But I'm having two problems:

  • 我可以在 options 中包含 .old ,但是没有 .old 值到达.出现的唯一值是 .initial 值(当我们订阅时)和 .new 值(每次观察到的属性更改时).我可以禁止显示 .initial 值,但是我不能禁止显示 .new 值或添加 .old 值.

  • I can include .old in the options, but no .old value ever arrives. The only values that appear are the .initial value (when we subscribe) and the .new value (each time the observed property changes). I can suppress the .initial value but I can't suppress the .new value or add the .old value.

如果 options [.initial,.new] (默认设置),我看不出有什么方法可以区分我接收的值是否是 .initial .new .使用真实的" KVO,我得到一个NSKeyValueChangeKey或NSKeyValueObservedChange,它们告诉我所得到的.但是,对于Combine发行商,我没有.我只是获得了未标记的值.

If the options are [.initial, .new] (the default), I see no way to distinguish whether the value I'm receiving is .initial or .new. With "real" KVO I get an NSKeyValueChangeKey or an NSKeyValueObservedChange that tells me what I'm getting. But with the Combine publisher, I don't. I just get unmarked values.

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

It seems to me that these limitations make this publisher all but unusable except in the very simplest cases. Are there any workarounds?

推荐答案

要获取旧值,我唯一能找到的解决方法是使用 .prior 而不是 .old,这会导致发布者在更改之前先发出属性的当前值,然后使用 collect(2).

For getting the old value, the only workaround I was able to find was to use .prior instead of .old, which causes the publisher to emit the current value of the property before it is changed, and then combine that value with the next emission (which is the new value of the property) using collect(2).

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

For determining what's an initial value vs. a new value, the only workaround I found was to use first() on the publisher.

然后,我合并了这两个发布者,并将它们全部包装在一个不错的小函数中,该函数吐出了一个自定义的 KeyValueObservation 枚举,可让您轻松确定它是否是初始值,并为您提供旧值(如果它不是初始值).

I then merged these two publishers and wrapped it all up in a nice little function that spits out a custom KeyValueObservation enum that lets you easily determine whether it's an initial value or not, and also gives you the old value if it's not an initial value.

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

Full example code is below. Just create a brand new single-view project in Xcode and replace the contents of ViewController.swift with everything below:

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)\"" }
}

这篇关于如何使用合并框架NSObject.KeyValueObservingPublisher?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!

10-26 23:38