Репутація українського ІТ. Пройти опитування Асоціації IT Ukraine
×Закрыть

Как создать кастомный паблишер в Swift Combine

Если вы читаете эту статью, то скорее всего, вы уже успели попробовать с Combine на каком-либо пет-проекте. И я уверен, что вас появилось некоторые вопросы, касательно этого нового фреймворка. К примеру, как создать свой кастомный паблишер в Combine? И эта статья может помочь вам понять основы механизма создания кастомных паблишеров.

Если вы пробовали поработать с сетью используя Combine, то вы должны быть знакомы с URLSession.shared.dataTaskPublisher. Этот встроенный (начиная с iOS 13) паблишер отправляет результат ответа от сервера на запрос своему подписчику. Давайте, в качестве примера, попробуем реализовать свой собственный dataTaskPublisher для URLSession. Стоить заметить, что процесс создания любого паблишера достаточно общий. Так что, поняв этот механизм раз, вам не составит труда делать различные паблишеры для ваших нужд.

Прежде всего, давайте узнаем, из каких частей состоит каждая цепочка Publisher-Subscriber. Очевидно, что у нас есть паблишер — тот, кто дает нам значения, и подписчик, который запрашивает и принимает эти значения для своих собственных целей. А вот кто производит сами данные? Основная часть этого контракта обычно скрыта. Речь идет о подписке (Subscription), и именно здесь происходит вся магия.
Подписка похожа на Interactor, ViewModel или просто мозг в цепочке паблишер-подписчик. Сам паблишер создает подписку со всеми необходимыми зависимостями и передает ее подписчику. После этого подписка начинает действовать и обрабатывать значения с течением времени. Затем подписчик принимает эти значения, и мы можем доступиться к ним прямо в sink { $0 } кложуре (или в assing, если тип ошибки Never).

Давайте подведем итоги. Для создания рабочего потока данных в Combine нам понадобятся три вещи:

  • Publisher.
  • Subscription.
  • Subscriber.

Подписка (Subscription)

Начнем с основной части — подписки. Все нужные классы и структуры будем создавать в Publishers экстеншне. Подписку DataSubscription будем имплементировать в виде класса, чтобы получить возможность передавать ее по ссылке. Наш класс будет иметь несколько приватных полей: URLSession, Subscriber и Request. Все эти свойства будут прокинуты через инициализатор, а предоставит их наш Publisher. Кроме того, для лучшей инкапсуляции, можно пометить класс DataSubscription как приватный, чтобы к нему нельзя было просто так доступиться из Publishers экстеншна.

extension Publishers {
    
    class DataSubscription<S: Subscriber>: Subscription where S.Input == Data, S.Failure == Error {
        private let session = URLSession.shared
        private let request: URLRequest
        private var subscriber: S?
        
        init(request: URLRequest, subscriber: S) {
            self.request = request
            self.subscriber = subscriber
            sendRequest()
        }
        
        func request(_ demand: Subscribers.Demand) {
            //TODO: - Optionaly Adjust The Demand
        }
        
        func cancel() {
            subscriber = nil
        }
        
        private func sendRequest() {
            guard let subscriber = subscriber else { return }
            session.dataTask(with: request) { (data, _, error) in
                _ = data.map(subscriber.receive)
                _ = error.map { subscriber.receive(completion: Subscribers.Completion.failure($0)) }
            }.resume()
        }
    }
}

Наша DataSubscription — это дженерик класс, который реализует Subscription протокол и содержит кейс where, в котором говорится, что ассоциативный тип Input — это Data, а Failure — это Error. Внутри этого класса у нас есть приватный метод sendRequest, который содержит в себе dataTask с комплинш хендлером. Внутри колбека мы отправляем данные нашему подписчику в случае успешного ответа, либо шлем ему соответствующий Error в случае ошибки.

Еще одно требования протокола Subscription — это метод cancel, внутри которого, мы просто обнуляем ссылку на подписчика, чтобы разорвать цепочку. Таким образом, у нас будет возможность вручную отменить эту подписку в будущем. Выглядит достаточно просто. Теперь давайте создадим паблишера DataPublisher, который будет создавать подписку DataSubscription и отправлять ее подписчику.

Publisher

Если вы уже смотрели, как реализованы паблишеры в Combine, то вы скорее всего заметили, что они — обычные структуры (struct). Мы также сделаем структуру DataPublisher с одним обязательным для инициализации полем — urlRequest. Кроме того, наш кастомный паблишер должен быть подписан на Publisher протокол, чтобы иметь возможность нормально функционировать в мире Combine. После имплементации этого протокола, компилятор добавит generic метод receive с двумя typealias типами Output и Failure в вашу структуру DataPublisher. Для typealias Output мы укажем Data, а для Failure — Error соответсвенно. Хорошо, теперь нам нужно имплементировать сам метод receive.
Внутри этого метода нам нужно сделать две вещи: создать подписку DataSubscription и передать ее подписчику (subscriber). Вот как это выглядит в коде.

// MARK: - Data Publisher
extension Publishers {
    
    struct DataPublisher: Publisher {
        typealias Output = Data
        typealias Failure = Error
        
        private let urlRequest: URLRequest
        
        init(urlRequest: URLRequest) {
            self.urlRequest = urlRequest
        }
        
        func receive<S: Subscriber>(subscriber: S) where
            DataPublisher.Failure == S.Failure, DataPublisher.Output == S.Input {
                let subscription = DataSubscription(request: urlRequest,
                                                    subscriber: subscriber)
                subscriber.receive(subscription: subscription)
        }
    }
}

Теперь почти все готово, осталась самая малость. Чтобы наш подписчик мог получать значения из DataPublisher, мы должны создать какое-то свойство либо метод, который будет возвращать инстанс DataPublisher. Давайте расширим URLSession с помощью метода dataResponse, который примет URLRequest в качестве единственного параметра и вернет нам DataPublisher.

extension URLSession {
    func dataResponse(for request: URLRequest) -> Publishers.DataPublisher {
        return Publishers.DataPublisher(urlRequest: request)
    }
}

А сейчас мы можем просто вызвать URLSession.shared.dataResponse(for request: URLRequest), чтобы получить ответ в виде даных от удаленного сервера. Как видите, ничего сложного здесь нет. Теперь вы можете создавать собственные кастомные паблишеры для любого объекта, которого захотите. Полный проект можно найти на Github, также у меня есть еще один репозиторий под названием Combine-UIKit, где вы можете посмотреть реализацию паблишеров для различных компонентов из UIKit.

LinkedIn
Допустимые теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Допустимые теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Подписаться на комментарии