Синхронизация объединенных свойств в ReactiveSwift - PullRequest
0 голосов
/ 26 июня 2019

Я подумываю о преобразовании проекта с использованием собственной настраиваемой структуры сигналов для использования 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
}

Позже мы получаем некоторую информацию, которая заставляет насобновите одновременно значения a и b:

a.value = 3
b.value = 4

Проблема в том, что c сообщит своим слушателям, что у него есть значения 3 -> 5 -> 7.5 является полностью ложным и не представляет действительное состояние, поскольку мы никогда не хотели состояния, в котором a было равно 3, а b было равно 2.

Есть ли способ обойти это?Способ подавления обновлений Property при обновлении всех его зависимостей до новых состояний и пропуска обновления только после того, как вы закончите?

1 Ответ

0 голосов
/ 27 июня 2019

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
...