combineLatest
Фундаментальная цель * состоит в том, чтобы отправить значение, когда какой-либо из его входных данных отправит новое значение, поэтому я не думаю, что есть способ избежать этой проблемы, если вы хотите использовать этот оператор.
Если важно, чтобы оба значения обновлялись по-настоящему одновременно, рассмотрите возможность использования MutableProperty<(Int, Int)>
или размещения двух значений в структуре. Если вы дадите немного больше контекста о том, что вы на самом деле пытаетесь достичь, то, возможно, мы могли бы дать лучший ответ.
Приостановка обновлений
Так что я действительно не рекомендую делать что-то подобное, но если вы хотите использовать технику общего назначения для «приостановки» обновлений, вы можете сделать это с помощью глобальной переменной, указывающей, приостановлены ли обновления, и оператором 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
Если 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
Используйте 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
}
И вы могли бы даже иметь удобные свойства для прямого доступа к 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
}
}
С этим вы можете создавать изменяемые версии a
и b
, которые позволяют легко и просто получать и устанавливать значения:
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, вы даже можете использовать новую функцию ключа @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