Разделение 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: уменьшить:.

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

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...
}];

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

[timer connect];

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


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

  • Теперь вычисления значений поточно-безопасные, потому что они не зависят от стороны последствия. Если вам нужно реализовать что-то более дорогое, это очень просто для перемещения важной работы в фоновый поток.
  • Аналогично, вычисления значений независимы друг от друга. Oни могут быть легко распараллелены, если это когда-нибудь станет ценным.
  • Вся логика теперь локальна для привязок. Вам не нужно удивляться где происходят изменения или беспокоиться о заказе (например, между инициализация и обновление), поскольку все это в одном месте и может быть прочитано сверху вниз.
  • Значения могут быть рассчитаны без ссылки на представление. Для например, в Model-View-ViewModel, счет и прогресс будут фактически определены в представлении модель, а затем уровень представления - это всего лишь набор немых привязок.
  • Измененные значения передаются из только одного входа. Если вам вдруг понадобится включить другой источник входного сигнала (например, реальный прогресс вместо таймера), там только одно место, которое вам нужно изменить.

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

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