Единый декларативный API для обработки значений с течением времени. Combine можно использовать для унификации и упрощения вашего кода для работы с такими вещами, как делегаты, уведомления, таймеры, блоки завершения и обратные вызовы. Некоторое время на iOS были сторонние реактивные фреймворки (RXSwift), но теперь Apple создала свои собственные.
В Combine есть несколько основных концепций, которые необходимо понять.
- Издатели и подписчики
- Операторы
- Объекты
Издатели
Это определяет, как создаются значения и ошибки. Типы значений, что означает, что мы используем struct. Издатели разрешают регистрацию подписчика.
Издатель предоставляет данные при их наличии и по запросу. Издатель, у которого не было запросов на подписку, не будет предоставлять никаких данных.
Publisher — это протокол, который имеет два связанных типа: первый — Output, который представляет собой тип значения, воспроизводимого издателем, а второй — Failure, который представляет собой разновидность ошибки, создаваемой издателем.
У издателя есть одна ключевая функция — подписка. Эта функция требует ввода подписчика в качестве параметра, чтобы соответствовать выходным данным издателей.
Combine предоставляет издателям ряд дополнительных удобств:
- Just
- Future
- Deferred
- Empty
- Sequence
- Fail
- Record
- Share
- Multicast
- ObservableObject
- @Published
Есть некоторые API Apple, помимо Combine, также предоставляют издатели —
А. SwiftUI использует свойства обертки @Publisher и @ObservedObject, предоставляемые Combine, для неявного создания издателя и поддержки его механизмов декларативного просмотра.
В. Foundation
- URLSession.dataTaskPublisher
- .publisher on KVO instance
- NotificationCenter
- Timer
- Result
Пример издателя для центра уведомлений
Подписчики
Подписчик несет ответственность за запрос данных и принятие данных (и возможных сбоев), предоставленных издателем.
Он получает значения и completion, это ссылочные типы, что означает классы.
Подписчик описывается двумя связанными типами: один для ввода и один для отказа. Подписчик инициирует запрос данных и контролирует объем получаемых данных. Это можно рассматривать как «движущую силу» в Combine, поскольку без подписчика другие компоненты остаются бездействующими.
Издатели и операторы бессмысленны, пока что-то не слушает опубликованные события. Это что-то и есть подписчик.
Подписчик получает от издателя поток значений, событий завершения или сбоя.
В Combine есть два встроенных подписчика, т.е. sink и assign. В SwiftUI встроен подписчик: onReceive.
Подписчики могут поддерживать отмену, которая завершает подписку и останавливает всю обработку потока до любого завершения, отправленного издателем. И Assign, и Sink соответствуют протоколу cancellable.
Примечание. Издатели и подписчики должны быть связаны и составляют основу Combine. Когда вы подключаете подписчика к издателю, оба типа должны совпадать, то есть выход на вход и отказ до отказа. Один из способов визуализировать это — как последовательность операций над двумя типами, параллельными, где оба типа должны совпадать, чтобы соединить компоненты вместе.
Sink
Это принимает клоужер, которое получает любые значения от издателя. Это позволяет разработчику завершить конвейер своим собственным кодом. Этот подписчик также чрезвычайно полезен при написании модульных тестов для проверки издателей или конвейеров.
let _ = Just("Anuj Rai") .map { (value) -> String in return value } .sink { (receivedValue) in print(receivedValue) }
Результатом вышеупомянутой функции будет “Anuj Rai”
В приведенном выше примере Sink-
Просто это издатель, который будет публиковать только выходные данные, и тип сбоя никогда не будет.
map — это оператор, который преобразует данные восходящего потока и будет выполнять функции и возвращать только выходные данные. Это не вернет никакой ошибки
Sink: этот метод создает подписчика и немедленно запрашивает неограниченное количество значений, которые получат возвращаемое значение от издателя.
Assign
Применяет значения, переданные от издателя, к объекту, определенному ключевым путем. Путь к ключевому путю устанавливается при создании конвейера.
Если assign используется для обновления элемента пользовательского интерфейса, вам необходимо убедиться, что он обновляется в главном потоке. Этот вызов гарантирует, что подписчик получен в главном потоке.
Для реализации Assign просто возьмите одну кнопку и одну метку в Storyboard. Создайте IBAction для кнопки с именем actionButtonTapped :. Когда пользователь щелкает по кнопке, количество нажатий должно сразу отображаться в виде текста метки. Я назвал метку «labelAssignSubscriber», а название кнопки — «tapButton». Есть одна переменная, в которой хранится счетчик нажатий. labelAssignSubscriberValueString — это издатель, который здесь быстро рассматривается как оболочка свойства. Просто введите код ниже в классе выше метода viewDidLoad.
@IBOutlet weak var labelAssignSubscriber: UILabel! @IBOutlet weak var tapButton: UIButton! var buttonTapCount: Int = 0 var cancellable: AnyCancellable? @Published var labelAssignSubscriberValueString: String? = "You dont tap the button" ///( Property wrapper can not be let)
Теперь создайте один метод, который будет использовать назначение подписчика.
private func publishAndSubscribeExampleWithAssign() { self.cancellable = self.$labelAssignSubscriberValueString.receive(on: DispatchQueue.main) .assign(to: \.text, on: self.labelAssignSubscriber) }
Теперь вызовите этот метод (publishAndSubscribeExampleWithAssign) из viewDidLoad и добавьте метод actionButtonTapped кнопки.
@IBAction func actionButtonTapped(_ sender: UIButton) { self.buttonTapCount = buttonTapCount + 1 self.labelAssignSubscriberValueString = "You have tapped the button \(self.buttonTapCount) time" }
Теперь вы увидите, как меняется текст метки при нажатии кнопки.
Subjects
Subjects — это также тип издателя, на который мы можем подписаться, но также динамически отправлять им события. Subjects предоставляет внешним вызывающим абонентам метод публикации элементов.
Subjects может использоваться для «вставки» значений в поток, вызывая его метод send (:). Это может быть полезно для адаптации существующего императивного кода к модели Combine.
Subjects также может передавать значения нескольким подписчикам. Если к теме подключено несколько подписчиков, при вызове send () значения будут переданы нескольким подписчикам.
В Combine есть два типа встроенных Subjects: PassthroughSubject, CurrentValueSubject.
PassthroughSubject
Если вы подпишетесь на него, вы будете получать все события, которые будут происходить после того, как вы подпишетесь. Когда он создается, определяются только типы. Когда подписчик подключен и запрашивает данные, он не получит никаких значений, пока не будет вызван вызов .send (). PassthroughSubject не поддерживает никакого состояния, он передает только указанные значения. Вызов .send () затем отправит значения всем подписчикам.
Посмотрите ниже пример PassThrough Subject —
func subjectExampleWithSendFunctionAndPublisher() { let subject = PassthroughSubject<String, Never>() let anyCancellable = subject .sink { value in print(value) } // Below send function is always used to send values to the subscriber.. subject.send("Sending first object") subject.send("Sending second object") /* //subscribe a subject to a publisher let _ = Just("world!") .subscribe(subject) */ // If above function will be uncommented then below code will not be executed. Because of Just publisher. It only emits an output to each subscriber just once, and then finishes subject.send("Sending Third object") let _ = Just("Publishing the Value for subject") .subscribe(subject) anyCancellable.cancel() }
AnyCanellable: тип AnyCancellable удаляет подписчика в общей форме Cancellable. Это чаще всего используется, когда вы хотите, чтобы ссылка на подписчика очистила его при освобождении.
Результатом вышеуказанной программы будет:
- Отправка первого объекта
- Отправка второго объекта
- Отправка третьего объекта
- Публикация значения предмета
Как я уже сказал, у сабжа может быть несколько подписчиков. Ниже приведен пример того же —
func subjectExampleWithMultipleSubscriber() { let subject = PassthroughSubject<String, Never>() let publisher = subject.eraseToAnyPublisher() let subscriber1 = publisher.sink(receiveValue: { value in print(value) }) //subscriber1 will recive the events but not the subscriber2 subject.send("Event1") subject.send("Event2") let subscriber2 = publisher.sink(receiveValue: { value in print(value) }) //Subscriber1 and Subscriber2 will recive this event subject.send("Event3") }
Результатом вышеуказанной программы будет:
- Event1
- Event2
- Event3
- Event3
Мы видели, что «Event3» было напечатано 2 раза. Почему ?? Поскольку есть два подписчика, мы уведомляем их, когда мы отправляем «Событие 3».
CurrentValueSubject
Действует аналогично PassthroughSubject, но также предоставляет новым подписчикам самый последний элемент. currentValueSubject создает экземпляр, к которому вы можете присоединить несколько подписчиков.
CurrentValueSubject запоминает текущее значение, поэтому при подключении подписчика он немедленно получает текущее значение. Когда подписчик подключается и запрашивает данные, отправляется начальное значение. Дальнейшие вызовы .send () впоследствии будут передавать значения любым подписчикам. Когда он создается (currentValueSubject), вы делаете это с начальным значением соответствующего типа вывода для Subject.
func subjectExampleOfCurrentSubject() { // This will print currentvalue (Anuj) then wil print sending values let subject = CurrentValueSubject<String, Never>("Anuj") let publisher = subject.eraseToAnyPublisher() let anuj = publisher.sink(receiveValue: { value in print(value) }) subject.send("Combine") subject.send("Swift") }
Результатом вышеуказанной программы будет:
- Anuj
- Combine
- Swift
Операторы
Операторы — удобное название для ряда встроенных функций, включенных в Publisher. Это описывает, когда и где доставлено конкретное событие.
Это поддерживается циклом выполнения и очередью отправки.
Операторов много, но мы обсудим некоторые из них.
Scan
Преобразует элементы вышестоящего издателя, предоставляя текущий элемент замыканию вместе с последним значением, возвращаемым замыканием. сюда входят два параметра — A. initialResult: предыдущий результат, возвращенный замыканием nextPartialResult. B. nextPartialResult: закрытие, которое принимает в качестве аргументов предыдущее значение, возвращаемое закрытием, и следующий элемент, переданный вышестоящим издателем.
Пример Scan:
func scanExample() { let _ = (0...5) .publisher .scan(0, { $0 + $1 }) .sink(receiveValue: { print ("\($0)", terminator: " ") }) }
Здесь сканирование даст значение нижестоящему, добавив предыдущее значение и текущее значение.
Результатом вышеуказанной функции будет:
0 1 3 6 10 15
Reduce
Издатель, который применяет замыкание ко всем полученным элементам и выдает накопленное значение, когда вышестоящий издатель завершает работу. Это очень похоже на функцию Scan. Основное различие между Scan и Reduce заключается в том, что Reduce не запускает никаких значений, пока вышестоящий издатель не завершит успешно. Как и при Scan, вам не нужно поддерживать тип вышестоящего издателя, но вы можете преобразовать тип в своем замыкании, возвращая все, что соответствует вашим потребностям.
Пример Reduce:
func reduceExample() { let _ = (0...5) .publisher .reduce(0, { prevVal, newValueFromPublisher -> Int in prevVal+newValueFromPublisher }) .sink(receiveValue: { print ("\($0)", terminator: " ") }) self.scanExample() }
Результатом этого будет: 15
CombineLatest
Издатель, который получает и объединяет новейшие элементы от двух издателей. Это обеспечивает обновление, когда любой из вышестоящих издателей предоставляет новое значение. Все вышестоящие издатели должны иметь один и тот же тип отказа. Тип вывода оператора — это кортеж типов вывода каждого из издателей. Например, если combLatest был использован для слияния издателя с типом вывода <String> и другого издателя с типом вывода <Int>, результирующий тип вывода будет кортежем (<String, Int>). Аналогичным образом CombineLatest3 и CombineLatest4 работают для 3-х издателей и 4-х издателей последнего элемента.
func combineLatestExample() { let usernamePublisher = PassthroughSubject<String, Never>() let passwordPublisher = PassthroughSubject<String, Never>() let validatedCredentials = Publishers.CombineLatest(usernamePublisher, passwordPublisher) .map { (username, password) -> (String, String) in return (username, password) } .map { (username, password) -> Bool in !username.isEmpty && !password.isEmpty && password.count > 12 } .eraseToAnyPublisher() let firstSubscriber = validatedCredentials.sink { (valid) in print("First Subscriber: CombineLatest: Are the credentials valid: \(valid)") } let secondSubscriber = validatedCredentials.sink { (valid) in print("Second Subscriber: CombineLatest: Are the credentials valid: \(valid)") } // Nothing will be printed yet as `CombineLatest` requires both publishers to have send at least one value. usernamePublisher.send("avanderlee") passwordPublisher.send("weakpass") passwordPublisher.send("verystrongpassword") }
Выше мы возвращаем bool на основе значений восходящего потока из CombineLatest и функции карты.
Результат будет:
- Первый подписчик: CombineLatest: действительны ли учетные данные: false
- Второй подписчик: CombineLatest: действительны ли учетные данные: false
- Первый подписчик: CombineLatest: действительны ли учетные данные: true
- Второй подписчик: CombineLatest: действительны ли учетные данные: true
Merge
Это берет двух вышестоящих издателей и смешивает опубликованные элементы в одном издателе (конвейере) по мере их получения. Все вышестоящие издатели должны иметь один и тот же тип вывода, а также один и тот же тип сбоя.
func mergeExample() { let germanCities = PassthroughSubject<String, Never>() let italianCities = PassthroughSubject<String, Never>() let mergePublisher = Publishers.Merge(germanCities, italianCities) .map({ (value) -> String in return value }) .eraseToAnyPublisher() let mergeSubscriber = mergePublisher.sink { (city) in print("\(city) is a city in europe") } germanCities.send("Munich") italianCities.send("Milano") }
Zip
Принимает двух вышестоящих издателей и смешивает опубликованные элементы в один конвейер, ожидая, пока значения не будут объединены в пары от каждого вышестоящего издателя, прежде чем пересылать пару как кортеж.
Все вышестоящие издатели должны иметь один и тот же тип отказа. Заметное отличие от CombineLatest заключается в том, что zip ожидает значений, поступающих от вышестоящих издателей, и публикует только один новый кортеж, когда новые значения были предоставлены всеми вышестоящими издателями.
Одним из примеров использования этого является ожидание, пока все потоки не предоставят одно значение для обеспечения точки синхронизации. Работает как группа диспетчеризации.
func zipExample() { let usernamePublisher = PassthroughSubject<String, Never>() let passwordPublisher = PassthroughSubject<String, Never>() let validatedCredentials = Publishers.Zip(usernamePublisher, passwordPublisher) .map { return $0} .sink { (mergedValue) in print("\(mergedValue)") } usernamePublisher.send("Rai55@32342") passwordPublisher.send("veryStrongPassword") passwordPublisher.send("veryStrongPassword2") // usernamePublisher.send("AnujRai890888@3234909") }
Будет напечатано: («Rai55 @ 32342», «veryStrongPassword»). почему не печатается второе значение passwordPublisher ???
Потому что нам также нужно отправить значение userNamePublisher. Так что просто раскомментируйте второй usernamePublisher и запустите. Вы получите результат вроде —
(“Rai55@32342”, “veryStrongPassword”)
(“AnujRai890888@3234909”, “veryStrongPassword2”)
Примечание. Если ваши вышестоящие издатели имеют один и тот же тип и вам нужен поток отдельных значений, а не кортежи, используйте оператор слияния. Если вы хотите дождаться значений от всех вышестоящих поставщиков, прежде чем предоставлять обновленное значение, используйте оператор Zip.
В следующем посте я расскажу, как использовать Combine для вызова API и обновления пользовательского интерфейса. Весь проект находится на гитхабе. Загрузите и запустите все функции одну за другой.