14

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

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

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

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

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

ответ

38

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

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

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

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

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

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

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

[timer connect]; 

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


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

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

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

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

+5

Человек, ты зверь! Жутко хорошо! Этот пост будет стоить значка «Звездный ответ», если бы мы были более крупным сообществом. – allprog

+0

@allprog Спасибо! :) –

+0

Что сказал @allprog! Вау. Спасибо - действительно не мог попросить лучшего объяснения. –

Смежные вопросы