【问题标题】:Synchronising combined Properties in ReactiveSwift在 ReactiveSwift 中同步组合属性
【发布时间】:2019-06-26 20:43:55
【问题描述】:

我正在考虑将使用我自己的自定义信号框架的项目转换为使用 ReactiveSwift,但有一个基本问题我从未弄清楚如何在 ReactiveSwift 中解决:

作为一个简化示例,假设您有两个可变属性:

let a = MutableProperty<Int>(1)
let b = MutableProperty<Int>(2)

然后,我们派生一个结合两者的属性来实现我们的逻辑:

let c = Property.combineLatest(a, b).map { a, b in
    return a + b
}

稍后,我们收到一些信息,导致我们同时更新ab 的值:

a.value = 3
b.value = 4

现在的问题是c 会通知它的听众它有3 -&gt; 5 -&gt; 7 的值。 5 完全是虚假的,并不代表有效的状态,因为我们从来不想要a 等于 3 而b 等于 2 的状态。

有没有办法解决这个问题?一种抑制对 Property 的更新同时将其所有依赖项更新为新状态并且仅在完成后才允许更新的方法?

【问题讨论】:

    标签: swift reactive-cocoa reactive-swift


    【解决方案1】:

    combineLatest 的基本目的是在其上游输入发送一个新值时发送一个值,所以如果你想使用该运算符,我认为没有办法避免这个问题。

    如果两个值真正同时更新很重要,那么考虑使用MutableProperty&lt;(Int, Int)&gt; 或将这两个值放在一个结构中。如果您提供更多有关您实际尝试完成的内容的背景信息,那么也许我们可以提供更好的答案。

    暂停更新

    所以我真的不建议这样做,但是如果您想要一种通用技术来“暂停”更新,那么您可以使用指示更新是否暂停的全局变量和 filter 运算符来实现:

    let a = MutableProperty<Int>(1)
    let b = MutableProperty<Int>(2)
    
    var pauseUpdates = false
    
    let c = Property.combineLatest(a, b)
        .filter(initial: (0, 0)) { _ in !pauseUpdates }
        .map { a, b in
            return a + b
        }
    
    func update(newA: Int, newB: Int) {
        pauseUpdates = true
        a.value = newA
        pauseUpdates = false
        b.value = newB
    }
    
    c.producer.startWithValues { c in print(c) }
    
    update(newA: 3, newB: 4)
    

    但可能有更好的特定于上下文的解决方案来实现您想要实现的任何目标。

    使用采样器手动触发更新

    另一种解决方案是使用sample 运算符手动选择何时取值:

    class MyClass {
        let a = MutableProperty<Int>(1)
        let b = MutableProperty<Int>(2)
    
        let c: Property<Int>
    
        private let sampler: Signal<Void, Never>.Observer
    
        init() {
            let (signal, input) = Signal<Void, Never>.pipe()
            sampler = input
    
            let updates = Property.combineLatest(a, b)
                .map { a, b in
                    return a + b
                }
                .producer
                .sample(with: signal)
                .map { $0.0 }
    
            c = Property(initial: a.value + b.value, then: updates)
        }
    
        func update(a: Int, b: Int) {
            self.a.value = a
            self.b.value = b
            sampler.send(value: ())
        }
    }
    
    let x = MyClass()
    x.c.producer.startWithValues { c in print(c) }
    
    x.update(a: 3, b: 4)
    

    使用zip

    如果ab 总是一起改变,您可以使用zip 运算符等待两个输入都有新值:

    let a = MutableProperty<Int>(1)
    let b = MutableProperty<Int>(2)
    
    let c = Property.zip(a, b).map(+)
    
    c.producer.startWithValues { c in print(c) }
    
    a.value = 3
    b.value = 4
    

    zip 与每种更新类型的方法一起使用

    class MyClass {
        let a = MutableProperty<Int>(1)
        let b = MutableProperty<Int>(2)
    
        let c: Property<Int>
    
        init() {
            c = Property.zip(a, b).map(+)
        }
    
        func update(a: Int, b: Int) {
            self.a.value = a
            self.b.value = b
        }
    
        func update(a: Int) {
            self.a.value = a
            self.b.value = self.b.value
        }
    
        func update(b: Int) {
            self.a.value = self.a.value
            self.b.value = b
        }
    }
    
    let x = MyClass()
    x.c.producer.startWithValues { c in print(c) }
    
    x.update(a: 5)
    x.update(b: 7)
    x.update(a: 8, b: 8)
    

    将值组合到一个结构中

    我想我会提供一个这样的例子,即使你说你不想这样做,因为MutableProperty 有一个modify 方法,它比你想象的要简单地进行原子更新:

    struct Values {
        var a: Int
        var b: Int
    }
    
    let ab = MutableProperty(Values(a: 1, b: 2))
    
    let c = ab.map { $0.a + $0.b }
    
    c.producer.startWithValues { c in print(c) }
    
    ab.modify { values in
        values.a = 3
        values.b = 4
    }
    

    您甚至可以使用方便的属性直接访问ab,即使ab 属性是事实的来源:

    let a = ab.map(\.a)
    let b = ab.map(\.b)
    

    创建一种新类型的可变属性来包装复合属性

    您可以创建一个符合 MutablePropertyProtocol 的新类,以使其更符合人体工程学地使用结构来保存您的值:

    class MutablePropertyWrapper<T, U>: MutablePropertyProtocol {
        typealias Value = U
    
        var value: U {
            get { property.value[keyPath: keyPath] }
            set {
                property.modify { val in
                    var newVal = val
                    newVal[keyPath: self.keyPath] = newValue
                    val = newVal
                }
            }
        }
    
        var lifetime: Lifetime {
            property.lifetime
        }
    
        var producer: SignalProducer<U, Never> {
            property.map(keyPath).producer
        }
    
        var signal: Signal<U, Never> {
            property.map(keyPath).signal
        }
    
        private let property: MutableProperty<T>
        private let keyPath: WritableKeyPath<T, U>
    
        init(_ property: MutableProperty<T>, keyPath: WritableKeyPath<T, U>) {
            self.property = property
            self.keyPath = keyPath
        }
    }
    

    有了这个,您可以创建ab 的可变版本,这使得获取和设置值变得非常简单:

    struct Values {
        var a: Int
        var b: Int
    }
    
    let ab = MutableProperty(Values(a: 1, b: 2))
    
    let a = MutablePropertyWrapper(ab, keyPath: \.a)
    let b = MutablePropertyWrapper(ab, keyPath: \.b)
    
    let c = ab.map { $0.a + $0.b }
    
    c.producer.startWithValues { c in print(c) }
    
    // Update the values individually, triggering two updates
    a.value = 10
    b.value = 20
    
    // Update both values atomically, triggering a single update
    ab.modify { values in
        values.a = 30
        values.b = 40
    }
    

    如果您安装了 Xcode 11 Beta,您甚至可以使用基于新密钥路径的 @dynamicMemberLookup 功能使其更符合人体工程学:

    @dynamicMemberLookup
    protocol MemberAccessingProperty: MutablePropertyProtocol {
        subscript<U>(dynamicMember keyPath: WritableKeyPath<Value, U>) -> MutablePropertyWrapper<Value, U> { get }
    }
    
    extension MutableProperty: MemberAccessingProperty {
        subscript<U>(dynamicMember keyPath: WritableKeyPath<Value, U>) -> MutablePropertyWrapper<Value, U> {
            return MutablePropertyWrapper(self, keyPath: keyPath)
        }
    }
    

    现在代替:

    let a = MutablePropertyWrapper(ab, keyPath: \.a)
    let b = MutablePropertyWrapper(ab, keyPath: \.b)
    

    你可以写:

    let a = ab.a
    let b = ab.b
    

    或者直接设置值而不创建单独的变量:

    ab.a.value = 10
    ab.b.value = 20
    

    【讨论】:

    • 我在许多不同的情况下都遇到过这个问题。在许多情况下,如果不创建非常笨拙的代码,将值捆绑在一起是不可能的。我想要做的是结合两个不同的属性,并根据周围世界发生的任何事情进行原子更新,而当我知道有几件事情会快速连续更新时,不会触发虚假更新。我很乐意使用其他运算符,但我认为不存在其他任何运算符。
    • @DagÅgren 我更新了我的答案。但是如果你能给我一两个具体的例子来说明这个问题出现在哪里,我可能会给出一个更惯用的解决方案。如果您经常遇到这个问题,那么您可能在代码结构方面与框架的斗争有点过头了。
    • 使用全局标志似乎很笨拙。您认为应该可以将其实现为某种运算符。
    • 我添加了一些其他选项。但就像我说的那样,对于任何给定的具体示例,可能都有更好的具体答案。
    • 添加了更多。 FWIW,这很丑的原因是因为ReactiveSwift 基本上是为声明式创建数据流而设计的,并且很难以一般方式声明式地表达“根据周围世界发生的事情进行原子更新”,尽管具体案例可能会有更好的解决方案。
    猜你喜欢
    • 2017-11-18
    • 2019-12-01
    • 2012-07-25
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多