Combine: часть 2. Вершина айсберга

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Привет, Хабр! На связи Сергей, iOS-разработчик в компании SimbirSoft.

В прошлой статье мы познакомились с концепцией реактивного программирования с использованием фреймворка Combine и сравнили его с RxSwift.

Интеграция Combine в проект будет полезна для обеих сторон — бизнеса и команды разработки:

  1. Процесс работы становится быстрее, а значит дешевле.

  2. Фреймворк считается достаточно модным. Есть большое количество разработчиков, которым нравится использовать реактивный подход и которые хотят поработать с новым инструментом — повышается интерес соискателей к вакансиям, в которых фигурирует эта технология.

  3. Combine способствует легкой поддержке и удобном переходе на современные технологии в будущем (например, на SwiftUI).

 В этой части мы более подробно разберем виды основных компонентов Combine, а также их отличия и способы применения.

Паблишеры

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

В рамках этой статьи мы не будем подробно разбирать атрибут @Published, а также доступные «из коробки» паблишеры — NotificationCenter, URLSession, Timer.

Чем мне не понравился атрибут @Published? Тем, что он чаще всего используется в приложениях на SwiftUI, а значит, его интереснее было бы рассмотреть вместе с остальными атрибутами. Да и мне хочется сделать статью более универсальной.

А что не так с вышеперечисленными паблишерами? NotificationCenter довольно прост, и о нем толком нечего рассказывать. URLSession либо покрывается пачкой абстракций, либо  используют сторонние фреймворки. Timer нужен только для редких и очень специфичных задач.

Тем не менее, чтобы совсем не оставлять вас без информации, немного пройдусь по нашим персонам нон грата.

@Published — это атрибут, который позволяет из любого свойства сделать паблишер. Чаще всего он используется именно в приложениях на SwiftUI, поскольку плохо дружит с UIKit-ом. Дело в том, что он работает «под капотом» как willSet, и возникает риск, что данные не отобразятся на UI, даже если они есть (пример можно найти в документации Apple). Чуть подробнее про атрибут можно почитать тут.

NotificationCenter — простой как пробка паблишер. С ним можно создавать уведомления о каких-то событиях в одной части приложения и ловить в другой. Помимо этого он используется для того, чтобы не прокидывать комплишены через десяток абстракций, либо чтобы обойти ограничения архитектуры. Помимо собственных событий, можно отлавливать системные события, например, смены жизненного цикла. 

Старый способ использования:

    private func addOldObserverForNotification() {
        NotificationCenter.default
            .addObserver(
                self,
                selector: #selector(catchNotification),
                name: UIApplication.didEnterBackgroundNotification,
                object: nil
            )
    }

    @objc
    private func catchNotification() {
        print("catched old notification")
    }

Новый способ использования:

    private var bag = Set<AnyCancellable>()
    private func subscribeForNotification() {
        NotificationCenter.default
            .publisher(for: UIApplication.didEnterBackgroundNotification)
            .sink { notification in
                print(notification.name.rawValue)
            }
            .store(in: &bag)
    }

В чем плюсы нового подхода перед старым?

  1. Удобный для восприятия синтаксис. Если Combine используется повсеместно в приложении, код получается однородным и легко считываемым.

  2. Уходят легаси Objective-C селекторы, а с ними и запрет на использование аргументов в методах.

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

URLSession — паблишер для сетевых запросов. Его особенностью является «защита от дурака». То есть на любой оператор мы можем подписаться в главном потоке либо в бэкграунде с помощью оператора .subscribe(on: _). Поскольку для сетевых запросов загрузка данных в главном потоке не имеет смысла, оператор .subscribe(on: DispatchQueue.main) будет проигнорирован:

    private func fetchData(from url: URL) -> AnyPublisher<[String], Never> {
        URLSession.shared.dataTaskPublisher(for: url)
            .map { $0.data }
            .decode(type: [String].self, decoder: JSONDecoder())
            .replaceError(with: [])
            .subscribe(on: DispatchQueue.main)
            .receive(on: DispatchQueue.main)
            .eraseToAnyPublisher()
    }

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

Старый способ использования:

let timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in }
RunLoop.main.add(timer, forMode: .default)
timer.invalidate()

Новый способ использования:

let timer = Timer.publish(every: 1, on: .main, in: .default)

Как и в случае с NotificationCenter таймер, написанный через паблишер, убирает кложурку и делает код однороднее.

Его особенностью являются разные варианты запуска. Timer Publisher не будет нам отправлять никаких значений до тех пор, пока мы его не запустим. Тут есть 2 варианта: либо вызвать timer.connect() для мануального запуска таймера (например, при нажатии юзером кнопки «отправить код подтверждения»), либо с помощью метода timer.autoconnect(), который самостоятельно запустит таймер, как только на паблишер подпишется какой-нибудь подписчик:

Мануальный запуск

Автоматический запуск

timer

    .sink { secondsLeft in

        print(secondsLeft)

    }

    .store(in: &bag)

timer.connect()

timer

    .autoconnect()

    .sink { secondsLeft in

        print(secondsLeft)

    }

    .store(in: &bag)

Второе преимущество нового таймера в том, что ему не нужна ручная остановка (вызов метода .invaidate()).

При необходимости мы можем остановить и новый таймер, например, отменив подписку на паблишер. Метод .sink() возвращает объект с типом AnyCancellable — это класс-обертка, которая стирает тип данных подписчика.

У AnyCancellable есть 2 метода:

.cancel() — позволяет отменять подписку на паблишер. Мы можем его вызвать ручками при необходимости, но AnyCancellable может вызывать его автоматически при деинициализации объекта, если мы сохраним подписку с помощью второго метода.

.store(in:) — позволяет сохранить подписку на паблишер в коллекцию. 

Какие есть паблишеры в Combine

А теперь самое интересное — паблишеры можно разбить на разные категории в зависимости от критериев, по которым мы их делим.

По эмиту событий:

  1. One-shot — отдают значение всего 1 раз и замолкают

  2. Сontinuous broadcasting — отдают значения несколько раз в течение времени

По реакции на подписку:

  1. Hot — эгоистичные паблишеры. Им не важно, есть у них подписчики или нет. Если событие есть, они его отправят, даже если «на той стороне» некому слушать.

  2. Cold — будут смиренно ждать, пока на них кто-то подпишется, и только потом отправят событие.

По типу данных:

  1. Just (one-shot, cold)

  2. Empty (one-shot, cold)

  3. Optional (one-shot, cold)

  4. Result (one-shot, cold)

  5. Future (one-shot, hot)

  6. Sequence (cb, cold)

  7. Subject (cb, hot) — PassthroughSubject, CurrentValueSubject

  8. NotificationCenter (cb, hot)

  9. URLSession (one-shot, cold)

  10. Timer (cb, hot/cold)

  11. Кастомные типы данных, подписанные под протокол Publisher

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

completion — уведомляет о том, что паблишер закончил генерировать данные и больше ничего не пришлет.

value — это и есть данные, которе подписчик получает от паблишера.

    .sink(
        receiveCompletion: {
            print("status: \($0)")
        },
        receiveValue: {
            print("value: \($0)")
        }
    )

Optional

Optional — это паблишер, который генерирует событие всего один раз, а потом замолкает.

Его особенностью является то, что если value == nil, то паблишер вообще ничего не пришлет, а в блоке recieveCompletion уведомит о том, что он «всё».

var intValue: Int? = nil
let optionalPublisher: Optional<Int>.Publisher = intValue.publisher

optionalPublisher
    .sink(
        receiveCompletion: { completion in
            print("completion status: \(completion)")
        }, receiveValue: { value in
            print("received value: \(value)")
        }
    )

Консоль:

completion status: finished

В то же время, если значение не будет равно nil, то в блоке receiveValue оно уже будет извлечено и нам не надо будет использовать оператор guard или if-let.

var intValue: Int? = 10

Консоль:

received value: 10
completion status: finished

Опционал — это довольно редкий паблишер, который обычно вообще не используется на проектах — вместо него используется Just.

Just

Just — это паблишер, который генерирует событие всего один раз, а потом замолкает.

Используется в качестве начальной цепочки Data stream:

let stringPublisher: Just<String> = Just("						
Источник: https://habr.com/ru/companies/simbirsoft/articles/731918/


Интересные статьи

Интересные статьи

В этой статье поговорим о том, как работают сети на основе радиоканала и какие существуют атаки в зависимости от используемых технологий. Разберемся как можно "слушать" сеть в сыром виде, обнаруживая ...
У любителей электроники, разобравшихся в общих чертах в процессах отражения сигнала от концов линии передачи, неизбежно возникает ряд вопросов, связанных с выбором номиналов согласующих резисторов. Пр...
Вторая часть перевода лонгрида посвященного визуализации концепций из теории информации. Во второй части рассматриваются энтропия, перекрестная энтропия, дивергенция Кульбака-Лейблера, взаимн...
В первой части я рассказал о проблемах, с которыми я столкнулся в процессе создания 3D игры под браузер c использованием Three.js. Теперь я хотел бы подробно остановиться на решении некоторых важ...
В предыдущей статье были рассмотрены варианты, на что можно заменить существующие системы в рамках выполнения приказа об импортозамещении. Далее в статьях речь пойдет о выборе конкретных продукто...