Лучший мир с ReactiveCocoa

в 12:47, , рубрики: functional programming, iOS, objective-c, OS X, reactivecocoa, разработка под iOS, функциональное программирование, метки: , , , ,

Большинство приложений тратят много времени на ожидание событий и их обработку. Они ожидают взаимодействия пользователя с интерфейсом. Ожидают ответа на сетевой запрос. Ожидают завершения асинхронных операций. Ждут изменения зависимого значения. И только потом они реагируют.

Все эти ожидания и реакции можно реализовать большим количеством способов. Нам становится трудно рассуждать о них, связывать и создавать в любой высокоуровневой форме. Но мы можем сделать лучше.

Именно поэтому мы решили сделать общедоступной часть волшебства, стоящей за GitHub for Mac: ReactiveCocoa (RAC). RAC — это framework для компановки и преобразования последовательностей значений.

Что же это на самом деле?

Давайте конкретизируем. ReactiveCocoa предоставляет множество интересных возможностей:

  1. Возможность составлять операции над будущими данными.
  2. Способ уменшения количества состояний и мутабельности.
  3. Декларативный способ определения поведений и взаимосвязей между свойствами.
  4. Унифицированный, высокоуровневый интерфейс для асинхронных операций.
  5. Прекрасное API, основанное на KVO.

Все это может казаться немного хаотичным до того, пока вы не осознаете, что RAC предназначен для тех ситуаций, где вы ожидаете какое-то новое значение, а затем реагируете на его изменение.

Настоящая красота RAC в том, что он может приспосабливаться к различным, часто встречающимся ситуациям.

Хватит разговоров. Давайте посмотрим как это выглядит на самом деле.

Примеры

RAC, используя KVO, может предоставлять последовательность значений из KVO-совместимых свойств. Например, мы можем наблюдать за изменениями свойства username:

[RACObserve(self, username) subscribeNext:^(NSString *newName) {
    NSLog(@"%@", newName);
}];

Это круто, но это не более чем хорошее API над KVO. Действительно интересные вещи происходят, когда мы объединяем последовательности для выражения сложного поведения.

Предположим, мы хотим проверить ввел ли пользователь уникальное имя, но только с первых трех попыток:

[[[[RACObserve(self, username) 
    distinctUntilChanged] 
    take:3] 
    filter:^(NSString *newUsername) {
        return [newUsername isEqualToString:@"joshaber"];
    }] 
    subscribeNext:^(id _) {
        NSLog(@"Hi me!");
    }];

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

И что?

Подумайте о том, что нам пришлось бы сделать, чтобы выполнить это без RAC. А именно:

  • Использовать KVO, чтобы добавить наблюдателя за username.
  • Добавить свойство для запоминания последнего значения, полученного с помощью KVO, благодаря чему мы сможем игнорировать несущественные изменения.
  • Добавить свойство для подсчета количества полученных уникальных значений.
  • Увеличивать это свойство каждый раз, когда мы получаем уникальное значение.
  • И наконец перейти к сравнению.

RAC позваляет нам делать то же самое с меньшим количеством состояний, меньшим количеством шаблонного кода, лучшей локализованостью кода и с более четким выражением наших намеренней.

Что еще?

Мы можем комбинировать последовательности:

[[RACSignal 
    combineLatest:@[RACObserve(self, password), RACObserve(self, passwordConfirmation)] 
    reduce:^id(NSString *currentPassword, NSString *currentConfirmPassword) {
        return [NSNumber numberWithBool:[currentConfirmPassword isEqualToString:currentPassword]];
    }] 
    subscribeNext:^(NSNumber *passwordsMatch) {
        self.createEnabled = [passwordsMatch boolValue];
    }];

Каждый раз, когда свойства password или passwordConfirmation меняются, мы объеденяем два их последних значения и приводим их к BOOL, чтобы проверить совпадают ли они. Затем мы активируем или деактивируем кнопку create, используя полученный результат.

Связи

Мы можем применять RAC для получения мощных связей с условиями и трансформациями:

RAC(self, helpLabel.text) = [[RACObserve(self, help) 
        filter:^(NSString *newHelp) {
            return newHelp != nil;
        }] 
        map:^(NSString *newHelp) {
            return [newHelp uppercaseString];
        }];

Это связывает текст нашего help label’a к help свойству, если help свойство не nil, и после этого переводит строку в верхний регистр (потому что пользователям нравится, когда на них ОРУТ).

Асинхронность

RAC также довольно хорошо согласуется с асинхронными операциями.

Например, мы можем вызвать блок, как только несколько параллельных операций будут выполнены:

[[RACSignal 
    merge:@[ [client fetchUserRepos], [client fetchOrgRepos] ]] 
    subscribeCompleted:^{
        NSLog(@"They're both done!");
    }];

Или связать асинхронные операции:

[[[[client 
    loginUser] 
    flattenMap:^(id _) {
        return [client loadCachedMessages];
    }]
    flattenMap:^(id _) {
        return [client fetchMessages];
    }]
    subscribeCompleted:^{
        NSLog(@"Fetched all messages.");
    }];

Это авторизирует нас, получает кешированные сообщения, загружает сообщения с сервера и после выведет “Fetched all messages.”.

Мы так же достаточно просто можем перенести работу в фоновую очередь:

[[[[[client 
    fetchUserWithUsername:@"joshaber"] 
    deliverOn:[RACScheduler scheduler]]
    map:^(User *user) {
        // this is on a background queue
        return [[NSImage alloc] initWithContentsOfURL:user.avatarURL];
    }]
    deliverOn:RACScheduler.mainThreadScheduler]
    subscribeNext:^(NSImage *image) {
        // now we're back on the main queue
        self.imageView.image = image;
    }];

Или справиться с потенциальным условиями перехвата.

Например, мы можем обновить свойство результатом любого асинхронного вызова, но только если оно не было изменено до его завершения:

[[[self 
    loadDefaultMessageInBackground]
    takeUntil:[RACObserve(self.message) skip:1]]
    toProperty:@keypath(self.message) onObject:self];

Как это работает?

RAC довольно прост. Он весь состоит из сигналов.

Подписчики подписаны на сигналы. Сигналы отправляют своим подписчикам next, error, и completed события. И если это всего лишь сигналы, отправляющие события, ключевым вопросом становиться: когда же эти события отправляются?

Создание сигналов

Сигналы определяют свое поведение относительно того когда и в связи с чем отправлются события. Мы можем создать свой собственный сигнал, используя +[RACSignal createSignal:]:

RACSignal *helloWorld = [RACSignal createSignal:^(id<RACSubscriber> subscriber) {
    [subscriber sendNext:@"Hello, "];
    [subscriber sendNext:@"world!"];
    [subscriber sendCompleted];
    return nil;
}];

Блок, что мы передаем в +[RACSignal createSignal:], вызывается каждый раз, когда сигнал получает нового подписчика. Новый подписчик подставляется в блок и, таким образом, мы можем отправлять ему события. В примере выше мы создали сигнал, который отправляет “Hello, ”, потом “world!”, и после этого завершается.

Вложенные сигналы

Мы также можем создать другой сигнал, основанный на нашем helloWorld сигнале:

RACSignal *joiner = [RACSignal createSignal:^(id<RACSubscriber> subscriber) {
    NSMutableArray *strings = [NSMutableArray array];
    return [helloWorld subscribeNext:^(NSString *x) {
        [strings addObject:x];
    } error:^(NSError *error) {
        [subscriber sendError:error];
    } completed:^{
        [subscriber sendNext:[strings componentsJoinedByString:@""]];
        [subscriber sendCompleted];
    }];
}];

Теперь у нас есть сигнал joiner. Когда кто-то подписывается на joiner, он автоматически подписывается на сигнал helloWorld. Joiner добавляет все значения, полученные из helloWorld и потом, когда helloWorld завершается, он объединяет все полученные строки в единственную строку, отправляет ее и завершается.

Таким образом, мы можем создавать сигналы, основанные друг на друге, для выражения сложного поведения.

RAC включает в себя набор операций над RACSignal, которые именно этим и занимаются. Они принимают исходный сигнал, возвращая новый сигнал с некоторым определенным поведением.

Дополнительная информация

ReactiveCocoa работает как на Mac, так и на iOS. Для получения дополнительной информации Вы можете ознакомится с README, а так же с документацией, которую вы сможете найти в проекте ReactiveCocoa.

Для .NET разработчиков все это может показаться очень знакомым. ReactiveCocoa, на самом деле, Objective-C версия Reactive Extensions (Rx) для .NET.

Многие принципы Rx применимы и для RAC. Вот несколько действительно хороших источников о Rx:
Reactive Extensions MSDN entry
Reactive Extensions for .NET Introduction
Rx — Channel 9 videos
Reactive Extensions wiki
101 Rx Samples
Programming Reactive Extensions and LINQ

Перевод публикуется с согласия автора оригинальной статьи и данного framework'а Josh Abernathy.

Автор: numen31337

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js