Что такое Publisher/Subscriber?ios-10

Основная концепция

Publisher и Subscriber — это фундаментальные компоненты фреймворка Combine, представляющие собой реализацию паттерна "Наблюдатель" (Observer) для реактивного программирования в Swift.

Аналогия из реального мира:

  • Publisher — это газета (источник информации)
  • Subscriber — это читатель (получатель информации)
  • Subscription — это подписка (связь между ними)

Детальные определения

Publisher

  • Протокол, определяющий источник данных
  • Может отправлять:
    • Значения (Output)
    • Завершающее событие (Completion: .finished или .failure(Error))
  • Базовый строительный блок реактивных потоков в Combine

Subscriber

  • Протокол, определяющий получателя данных
  • Получает:
    • Подписку (Subscription)
    • Значения (Input)
    • Завершающее событие (Completion)
  • Обрабатывает и реагирует на поступающие данные

Базовый пример

import Combine

// 1. Создаем Publisher
let publisher = Just("Hello, Combine!") // Издатель, который emits одно значение

// 2. Создаем Subscriber
let subscriber = Subscribers.Sink<String, Never>(
    receiveCompletion: { completion in
        switch completion {
        case .finished:
            print("Получено завершение")
        case .failure(let error):
            print("Ошибка: \(error)")
        }
    },
    receiveValue: { value in
        print("Получено значение: \(value)")
    }
)

// 3. Связываем Publisher и Subscriber
publisher.subscribe(subscriber)

Жизненный цикл Publisher-Subscriber

  1. Подписка (Subscription):

    • Subscriber вызывает subscribe(_:) у Publisher
    • Publisher создает Subscription и передает ее Subscriber через receive(subscription:)
  2. Запрос данных:

    • Subscriber запрашивает N значений через request(_:) на Subscription
  3. Отправка данных:

    • Publisher отправляет значения через receive(_:)
    • Может отправить completion через receive(completion:)
  4. Отмена (Cancellation):

    • Subscriber может отменить подписку через cancel()

Основные типы Publisher

1. Just

let just = Just(42)  // Издает одно значение и завершается

2. Future

let future = Future<String, Error> { promise in
    // Асинхронная операция
    promise(.success("Результат"))
}

3. PassthroughSubject

let subject = PassthroughSubject<String, Never>()
subject.send("Привет")  // Ручная отправка значений

4. CurrentValueSubject

let currentValue = CurrentValueSubject<Int, Never>(0)
print(currentValue.value)  // 0 - хранит текущее значение

Основные Subscriber

1. Sink

[1, 2, 3].publisher
    .sink(
        receiveCompletion: { print($0) },
        receiveValue: { print($0) }
    )

2. Assign

class MyClass {
    var value: String = "" {
        didSet { print(value) }
    }
}

let obj = MyClass()
["A", "B", "C"].publisher
    .assign(to: \.value, on: obj)

3. Custom Subscriber

struct MySubscriber: Subscriber {
    typealias Input = Int
    typealias Failure = Never

    func receive(subscription: Subscription) {
        subscription.request(.max(2))  // Запрашиваем 2 значения
    }

    func receive(_ input: Int) -> Subscribers.Demand {
        print("Получено: \(input)")
        return .none  // Не изменяем спрос
    }

    func receive(completion: Subscribers.Completion<Never>) {
        print("Завершено")
    }
}

Операторы

Между Publisher и Subscriber можно вставлять операторы для трансформации:

[1, 2, 3, 4, 5].publisher
    .filter { $0 % 2 == 0 }  // Только четные
    .map { $0 * 2 }          // Умножаем на 2
    .sink { print($0) }      // Выводим: 4, 8

Управление памятью

Важно хранить подписки, иначе они немедленно отменяются:

var cancellables = Set<AnyCancellable>()

func setupSubscription() {
    NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)
        .sink { _ in print("App became active") }
        .store(in: &cancellables)  // Сохраняем подписку
}

Практическое применение в iOS

1. Обработка пользовательского ввода

textField.publisher(for: .editingChanged)
    .map { $0.text ?? "" }
    .debounce(for: .seconds(0.5), scheduler: RunLoop.main)
    .sink { [weak self] in self?.search($0) }
    .store(in: &cancellables)

2. Комбинирование сетевых запросов

Publishers.CombineLatest(
    fetchUserProfile(),
    fetchUserSettings()
)
.sink { profile, settings in
    // Обновляем UI
}
.store(in: &cancellables)

3. Реактивное обновление UI

@Published var isLoading: Bool = false

func loadData() {
    isLoading = true
    networkService.fetchData()
        .receive(on: DispatchQueue.main)
        .sink { [weak self] _ in
            self?.isLoading = false
        } receiveValue: { [weak self] data in
            self?.updateUI(with: data)
        }
        .store(in: &cancellables)
}

Ошибки новичков

  1. Забывают хранить подписки:

    • Подписка уничтожается сразу без сохранения в cancellables
  2. Не учитывают поток выполнения:

    URLSession.shared.dataTaskPublisher(for: url)
        .sink { /* Обработка в фоновом потоке! */ }
    
  3. Используют неправильные операторы:

    • Путают map с flatMap
    • Не понимают разницу между merge и combineLatest

Резюмируем

Publisher/Subscriber в Combine — это мощная абстракция для:

  1. Реактивного программирования в iOS/macOS
  2. Управления асинхронными операциями
  3. Создания декларативных data pipelines

Ключевые моменты:

  • Publisher определяет источник данных
  • Subscriber реагирует на данные
  • Operators позволяют трансформировать поток
  • Subscriptions управляют жизненным циклом

Практические преимущества:

  • Уменьшение состояния в приложении
  • Упрощение асинхронного кода
  • Легкая композиция операций
  • Встроенная поддержка SwiftUI

Для эффективного использования:

  1. Всегда храните подписки
  2. Используйте правильные операторы
  3. Контролируйте потоки выполнения
  4. Начинайте с простых примеров

Publisher/Subscriber — это фундамент современного реактивного подхода в iOS разработке.