Sitemap
Arseni

Arseni

iOS Developer

5 min readJul 29, 2021

--

Введение в Reactive Swift

Reactive Swift — это фреймворк функционального реактивного программирования для Swift.

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

Данная статья призвана облегчить вам работу с Functional Reactive Programming (далее FRP) и ReactiveSwift с точки зрения обычной iOS-разработки. Мы начнем с базовых примитивов и будем развиваться дальше.

Итак, давайте начнем. Будем считать, что вы свободно владеете Swift, вы установили репозиторий Reactive Swift, запущен и готов к работе Swift Playground.

Расшифруем необходимые понятия

Сигналы — Signals,

События — Events,

Значения — Values,

Строка — String,

Наблюдатель — Observer

Сигналы, События, Значения

Допустим, вы хотите отправить куда-то строку (String). Говоря языком ReactiveSwift, эта строка — значение(Value). Это значение отправляется как событие(Event) с помощью наблюдателя(Observer), который передает его по сигналу(Signal). Все, кто заинтересован в этой строке, может подключиться к этому сигналу, чтобы получить ее.

String -> (Observer -> Signal) -> StringReceiver

На данный момент это выглядит как обычный delegate pattern с другой расшифровкой. Давайте копнем немного глубже. Итак, наблюдатель и сигнал идут рука об руку. Все сигналы имеют наблюдателя, который передает события через них: явно или не явно. Давайте создадим явную пару, используя удобный конструктор Signal’s pipe():

let pipe = Signal<String, Never>.pipe()

Это возвращает кортеж, дающий tag на вход Наблюдателя и на output Сигнала. Сигналы и Наблюдатели являются generics, и в данном случае мы специализируем их для работы со строковыми значениями и без обработки ошибок. Подробнее об ошибках остановимся позже.

Далее, с помощью pipe(трубы), и мы можем наблюдать значения, передаваемые через сигнал, используя к ней closure:

pipe.output.observeValues { value in
print("🚙 My other car is a \(value)")
}

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

pipe.input.send(value: "DeLorean")

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

Get Arseni’s stories in your inbox

Join Medium for free to get updates from this writer.

Все вместе это выглядит следующим образом:

let pipe = Signal<String, Never>.pipe()

pipe.output.observeValues { value in
print("My other car is a \(value)")
}

pipe.input.send(value: "DeLorean")

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

Одна из особенностей сигналов заключается в том, что к ним можно присоединить любое количество закрытий. То есть они могут наблюдаться более чем одной вещью. Таким образом, значение одного send(value:) может быть получено и использовано множеством различных способов и контекстов.

let pipe = Signal<String, Never>.pipe()

pipe.output.observeValues { value in
print("I got 99 problems but a \(value) ain't one")
}

pipe.output.observeValues { value in
UIPasteboard.general.string = value
}

pipe.output.observeValues { value in
UserDefaults.standard.set(value: value, forKey: "JustSummerThings")
}

pipe.input.send(value: "🏖")

Но что, если мы хотим прекратить наблюдение за сигналом? Другими словами, отключить одно закрытие. В обычной ситуации с паттерном делегата мы просто обнулим .delegate. В ситуации с многоадресным делегатом у нас есть функция, которую можно вызвать для удаления отдельного делегата. В мире ReactiveSwift, где нет отдельного объекта, а есть скорее закрытие, нам нужна более тонкая конструкция. Здесь на помощь приходит Disposable. Каждый раз, когда вы наблюдаете сигнал, вы получаете в ответ одноразовый объект.

let pipe = Signal<String, Never>.pipe()

let problemsDisposable = pipe.output.observeValues { value in
print("I got 99 problems but a \(value) ain't one")
}

let beersDisposable = pipe.output.observeValues { value in
print("The \(value) is my favourite place to sling code")
}

pipe.input.send(value: "🏖")
problemsDisposable.dispose()
pipe.input.send(value: "🗽")

Когда вы вызываете dispose() на Disposable, это отключает наблюдение, так что закрытие больше не срабатывает.

Серия неудачных событий

Сигналы могут не только передавать значения. Они также могут быть неудачными, прерванными или завершенными (.failed, .completed, и .interrupted). Для представления всех этих событий используется enum Event. Когда вы подписываетесь (.observe) на Сигнал, вы на самом деле наблюдаете за событиями, испускаемыми этим сигналом. Функция .observeValues — это удобство, которое позволяет нам обращать внимание только на события Event.value — в нашем случае это значения строк. Давайте посмотрим на какие случаи событий бывают.

Во-первых, .failed. Как ни странно, это событие используется для представления чего-то, что пошло не так. У этого события есть значение, связанное с типом Error. Это дает нам сильно типизированное и декларативное сообщение об ошибке. Но это еще не все. Если сигнал получает событие .failed, он прекращает выдавать дальнейшие события. Таким образом, это используется, если мы сталкиваемся с ситуацией, когда мы не можем продолжить работу и должны сообщить всем наблюдающим за сигналом, что пошло не так.

enum EmojiError: Error {
case nonLyrical
}

let pipe = Signal<String, EmojiError>.pipe()

pipe.output.observe { event in
switch event {
case let .value(value):
print("I got 99 problems but a \(value) ain't one")
case let .failed(error) where error == .nonLyrical:
print("Bro, do you even Jay Z?")
default:
break
}
}

pipe.input.send(value: "🏖")
pipe.input.send(value: "🍺")
pipe.input.send(error: .nonLyrical)
pipe.input.send(value: "🏖")

Обратите внимание, что последнее значение “🏖” не было отправлено, потому что сигналом был получен .failed . Примечание: обычно мы используем оператор, чтобы определить, что “🍺” — это неправильный эмодзи, и автоматически отказать. Подробнее об этом позже.

Далее у нас есть .completed. Это также приводит к остановке сигнала, но без ошибки. Обычно он используется для обозначения успеха — Сигнал завершился без проблем, и он нам больше не нужен.

let pipe = Signal<String, Never>.pipe()

pipe.output.observe { event in
switch event {
case let .value(value):
print(value)
case let .completed:
print("🤗")
default:
break
}
}

print("Hey barkeep, mix me something good")
pipe.input.send(value: "2 shots of Campari")
pipe.input.send(value: "Ice cubes")
pipe.input.send(value: "Soda")
pipe.input.send(value: "2 lime wedges")
pipe.input.sendCompleted()

Наконец, есть .interrupted, который очень похож на .completed, но используется для того, чтобы просто остановить Сигнал, а не указать на успех действия. Сейчас это может показаться бесполезным, но когда мы перейдем к операторам и цепочкам, это станет более понятным. В качестве примера представьте, что Сигнал используется для обработки массива изображений, но во время обработки пользователь покидает экран. Сигнал должен прекратить работу, но он не завершился и не выдал ошибку. Она была прервана. Таким образом, приложение может очистить все, что используется для обработки изображений, но оставить без внимания путь успеха, который показал бы предупреждение или что-то еще. Без каких-либо bool или if или внешнего состояния.

В каждом случае, где Сигнал останавливается (.failed, .completed, и .interrupted), он не будет испускать никаких дальнейших Событий, даже если его Наблюдатель попытается это сделать. Поэтому, если мы неуклюжи в нашем коде и попытаемся послать событие .value после завершения Сигнала, оно не будет послано — Сигнал уже остановлен.

И последнее замечание о сигналах, созданных вручную (как мы делали выше с .pipe): если у них все еще есть активные наблюдатели (closures, которые не были утилизированы), они не завершатся, пока не получат .completed, .failed или .interrupted. Очень важно проверить, правильно ли де-инициализируются ваши сигналы.

Всем спасибо за внимание, надеюсь кто то найдет в этом полезное !

Актуальные ссылки:

https://github.com/ReactiveCocoa/ReactiveSwift

https://github.com/ReactiveCocoa/ReactiveCocoa

--

--