Разделение RACSignal для устранения состояния

Я использую ReactiveCocoa для обновления UILabel пока UIProgressView отсчитывает:

NSInteger percentRemaining = ...;
self.progressView.progress = percentRemaining / 100.0;

__block NSInteger count = [self.count];

[[[RACSignal interval:0.05 onScheduler:[RACScheduler mainThreadScheduler]]
    take: percentRemaining]
    subscribeNext:^(id x) {
        count++;
        self.countLabel.text = [NSString stringWithFormat:@"%d", count];
        self.progressView.progress = self.progressView.progress - 0.01;
    } completed:^{
        // Move along...
    }];

Это работает достаточно хорошо, но я не особенно доволен count переменная или с чтением значения self.progressView.progress чтобы уменьшить его.

Я чувствую, что должен иметь возможность выплевывать сигнал и связывать свойства напрямую, используя RAC макро. Что-то вроде:

RACSignal *baseSignal = [[RACSignal interval:0.05 onScheduler:[RACScheduler mainThreadScheduler]]
                            take: percentRemaining]

RAC(self, countLabel.text) = [baseSignal
                                  map: ...
                                  ...

RAC(self, progressView.progress) = [baseSignal
                                        map: ...
                                        ...

...покажи, где я застрял. Я не могу понять, как сочинять RACSignal так что мне не нужно полагаться на переменную состояния.

Кроме того, я не уверен, где / как вводить // Move along... побочный эффект мне нужен, когда поток завершается.

Я уверен, что оба достаточно просты, если вы думаете, правильный путь, но любая помощь будет очень признательна.

1 ответ

Решение

Если вы сомневаетесь, посмотрите RACSignal + Operations.h и RACStream.h, потому что обязательно должен быть оператор для того, что вы хотите сделать. В этом случае основной отсутствующий фрагмент - -scanWithStart: redu:.

Прежде всего, давайте посмотрим на baseSignal, Логика останется в основном той же самой, за исключением того, что мы должны опубликовать соединение для нее:

RACMulticastConnection *timer = [[[RACSignal
    interval:0.05 onScheduler:[RACScheduler mainThreadScheduler]]
    take:percentRemaining]
    publish];

Это сделано для того, чтобы мы могли разделить один таймер между всеми зависимыми сигналами. Хотя baseSignal Вы также предоставили работающий режим, который воссоздает таймер для каждого подписчика (включая зависимые сигналы), что может привести к крошечным отклонениям в их срабатывании.

Теперь мы можем использовать -scanWithStart:reduce: увеличить countLabel и уменьшить progressView, Этот оператор берет предыдущие результаты и текущее значение и позволяет нам преобразовывать или комбинировать их так, как мы хотим.

В нашем случае, однако, мы просто хотим игнорировать текущее значение (NSDate Отправлено от +interval:), поэтому мы можем просто манипулировать предыдущим:

RAC(self.countLabel, text) = [[[timer.signal
    scanWithStart:@0 reduce:^(NSNumber *previous, id _) {
        return @(previous.unsignedIntegerValue + 1);
    }]
    startWith:@0]
    map:^(NSNumber *count) {
        return count.stringValue;
    }];

RAC(self.progressView, progress) = [[[timer.signal
    scanWithStart:@(percentRemaining) reduce:^(NSNumber *previous, id _) {
        return @(previous.unsignedIntegerValue - 1);
    }]
    startWith:@(percentRemaining)]
    map:^(NSNumber *percent) {
        return @(percent.unsignedIntegerValue / 100.0);
    }];

-startWith: приведенные выше операторы могут показаться излишними, но это необходимо для text а также progress установлены до timer.signal отправил что-нибудь.

Тогда мы просто воспользуемся обычной подпиской для завершения. Вполне возможно, что эти побочные эффекты могут быть превращены в сигналы, но трудно понять, не видя код:

[timer.signal subscribeCompleted:^{
    // Move along...
}];

Наконец, потому что мы использовали RACMulticastConnection выше, на самом деле ничего не будет стрелять. Соединения должны быть запущены вручную:

[timer connect];

Это соединяет все вышеперечисленные подписки и запускает таймер, поэтому значения начинают поступать в свойства.


Теперь это, очевидно, больше кода, чем императивный эквивалент, поэтому можно спросить, почему это стоит. Есть несколько преимуществ:

  1. Вычисления значений теперь поточнобезопасны, потому что они не зависят от побочных эффектов. Если вам нужно реализовать что-то более дорогое, очень просто перенести важную работу в фоновый поток.
  2. Аналогичным образом, значения расчетов не зависят друг от друга. Их можно легко распараллелить, если это когда-нибудь станет полезным.
  3. Вся логика теперь локальна для привязок. Вам не нужно удивляться, откуда происходят изменения, или беспокоиться о порядке (например, между инициализацией и обновлением), потому что все это в одном месте и может быть прочитано сверху вниз.
  4. Значения могут быть рассчитаны без каких-либо ссылок на представление. Например, в Model-View-ViewModel количество и ход выполнения будут фактически определены в модели представления, а затем слой представления - это просто набор немых привязок.
  5. Изменяющиеся значения вытекают только из одного входа. Если вам вдруг потребуется включить другой источник ввода (например, реальный прогресс вместо таймера), вам нужно изменить только одно место.

По сути, это классический пример императивного и функционального программирования.

Хотя императивный код может начинаться с меньшей сложности, его сложность возрастает в геометрической прогрессии. Функциональный код (и особенно функционально-реактивный код) может начинаться более сложно, но затем его сложность возрастает линейно - управлять им намного легче по мере роста приложения.

Другие вопросы по тегам