在ReactiveSwift中同步组合属性

在ReactiveSwift中同步组合属性,swift,reactive-cocoa,reactive-swift,Swift,Reactive Cocoa,Reactive Swift,我正在考虑将一个使用我自己的定制信号框架的项目转换为使用ReactiveSwift,但有一个根本性的问题我从来没有想到如何在ReactiveSwift中解决: 作为一个简化的示例,假设您有两个可变属性: let a = MutableProperty<Int>(1) let b = MutableProperty<Int>(2) 稍后,我们收到一些信息,导致我们同时更新a和b的值: a.value = 3 b.value = 4 现在的问题是,c将通知其侦听器它具有值

我正在考虑将一个使用我自己的定制信号框架的项目转换为使用ReactiveSwift,但有一个根本性的问题我从来没有想到如何在ReactiveSwift中解决:

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

let a = MutableProperty<Int>(1)
let b = MutableProperty<Int>(2)
稍后,我们收到一些信息,导致我们同时更新
a
b
的值:

a.value = 3
b.value = 4
现在的问题是,
c
将通知其侦听器它具有值
3->5->7
。5完全是虚假的,不代表有效状态,因为我们从不希望出现
a
等于3而
b
等于2的状态


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

CombineRelateTest
的基本目的是在其任何上游输入发送新值时发送值,所以如果你想使用这个操作符,我认为没有办法避免这个问题

<>如果两个值同时真正更新是很重要的,那么考虑使用<代码> MutableProperty < />代码,或者将两个值放入一个结构中。如果你能提供更多关于你真正想要完成的事情的背景,那么也许我们能给出一个更好的答案

暂停更新 因此,我不建议您这样做,但是如果您想要一种“暂停”更新的通用技术,那么您可以使用一个全局变量来指示是否暂停更新,并使用
过滤器
操作符:

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)
使用
zip
如果
a
b
总是同时更改,则可以使用
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
将值组合到一个结构中 我想我会提供一个这样的例子,即使你说你不想这样做,因为
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
}
您甚至可以使用方便的属性直接访问
a
b
,即使
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
    }
}
如果您安装了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 = ab.a
let b = ab.b
或者直接设置值而不创建单独的变量:

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

我在许多不同的场合遇到过这个问题。在许多情况下,如果不创建非常笨拙的代码,将值捆绑在一起是不可能的。我想做的是结合两种不同的属性,根据周围世界发生的任何事情进行原子更新,当我知道一些事情将快速连续更新时,不会触发虚假更新。我很乐意使用其他运算符,但我认为不存在任何其他运算符。@DagÅgren我更新了我的答案。但是,如果你能给我一个或两个具体的例子来说明这个问题是如何产生的,我可能会给出一个更为惯用的解决方案。如果您经常遇到这个问题,那么您可能在代码的结构方面与框架有点过多的冲突。使用全局标志似乎相当笨拙。你可能会想,这应该可以作为某种操作符来实现。我添加了一些其他选项。但是就像我说的,对于任何一个特定的例子,都可能有一个更好的具体答案。FWIW,这之所以难看,是因为
ReactiveSwift
基本上是为声明式创建数据流而设计的,并且很难用一般的声明方式表达“根据周围世界发生的事情进行原子更新”,尽管特定情况可能有更好的解决方案。
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
}
@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