Swift Combine

Combine - фреймворк, представленный в 2019 году вместе с SwiftUI.

Вообще говоря, SwiftUI построен на Combine. Если говорить в терминах MVVM, то View's обновляются автоматически при изменении соответствующих данных во ViewModel. При использовании SwiftUI не нужно прописывать обработчики событий, достаточно использовать State Objects. SwiftUI построен на основе взаимодействия publisher'ов и subscriber'ов между данными и их отображением.

Основными понятиями Combine являются:

  • publishers (издатель, поставщик информации), то есть некие переменные, которые могут изменять свое значение с течением времени,
  • operators (операторы), которые преобразовывают данные и
  • subscribers (подписчик), то есть некие переменные, которые эти обновленные данные будут получать.

1. Publishers

Самый простой пример publisher`а - это свойство помеченное @Publishedвнутри ViewModel: ObservableObject:

Swift Combine

Простыми словами, Publisher - это некая переменная, которая с течением времени обновляет свое значение и позволяет отслеживать факт изменения своего значения.

Swift Combine

2. Operators

Операторы - это методы, которые подписываются на publisher`а, получают данные, и предстают для дальнейшей передачи данных в виде как бы дочерних publisher`ов.

Операторы в коде фреймворка Combine выглядят точно также как модификаторы в SwiftUI, они назначаются элементу с синтаксисом через точку:

Swift Combine

Операторы позволяют создать цепочку дочерних publisher`ов, которая заканчивается подписчиком (subscriber). То есть каждый следующий оператор подписывается на предыдущий.

В качестве примере можно рассмотреть оператор .sink(), который создаёт запрос на неограниченное количество обновлений значения publisher`а.

Полный список операторов, которые можно применять к publisher`ам можнопосмотреть по ссылке.

3. Subscribers

Publisher отправляет значения, а подписчик (subscriber) - это тот, кто работает с получаемыми значениями. Publisher не может отправлять значения, пока подписчик не прикрепится у нему и не отправит запрос на получение данных. Подписчик контролирует частоту отправки данных.

Swift Combine

Возвращаясь к примеру с MVVM, когда мы создали свойство помеченное@Published внутри ViewModel: ObservableObject - мы создали Publisher'а. Далее в своем коде мы инициализируем экземпляр ViewModel, помечая его @ObservedObject или @StateObject и этим самым мы подписываемся на изменения свойств, обозначенных @Published:

Swift Combine

Простыми словами, Subscriber - это некоторая структура или класс (в нашем примере ContentView), которая получает доступ к значениям publisher`а и отслеживает его изменения.

Пример из реальной жизни

По этой ссылке находится регулярно обновляемый json-файл, который содержит информацию о курсе валют центробанка. В нашем примере мы будем делать приложение с курсом валют.

Создаём проект приложения для iOS. Будем использовать MVVM (если не знаете о чем речь, можно посмотреть тут). Первое, что нам нужно, это модель данных.

1. Model

Если перейдём по нашей ссылке, то увидим, структуру данных:

"Date": "2024-06-05T11:30:00+03:00", "PreviousDate": "2024-06-04T11:30:00+03:00", "PreviousURL": "\/\/www.cbr-xml-daily.ru\/archive\/2024\/06\/04\/daily_json.js", "Timestamp": "2024-06-05T12:00:00+03:00", "Valute": { "AUD": { "ID": "R01010", "NumCode": "036", "CharCode": "AUD", "Nominal": 1, "Name": "Австралийский доллар", "Value": 59.1834, "Previous": 59.399 }... и так далее

Создаём новый Swift-файл, и делаем модель данных на основе структуры json-файла:

Swift Combine

Лайфхак: чтобы вручную всё это не перепечатывать можно воспользоваться сайтомhttps://app.quicktype.io/, который как раз и делает модели из json-файлов. Просто копируете в него всё содержимое, задаёте язык и получаете результат.

Единственное что, обратите внимание: даты я задал как String (decoder не хочет их расшифровывать как Date), а свойство TimeStamp я переименовал в id, чтобы структура соответствовала протоколу Identifiable.

2. ViewModel

Основной файл, с которым мы будем работать, как вы понимаете, это ViewModel. Создаём новый Swift-файл. Имортируем фреймворк Combine:

Swift Combine

Создаём ViewModel и делаем его (или её) ObservableObject.

Далее создаем @Published свойство, в котором мы будем хранить наши данные:

Swift Combine

Далее создаем функцию getCurrencyData(link: String), которая будет:

  • принимать на вход строку (нашу ссылку https://www.cbr-xml-daily.ru/daily_json.js),
  • скачивать данные,
  • проверять данные,
  • расшифровывать их из json формата,
  • сохранять в нашу переменную currencyInfo и
  • делать сопутствующие Combine-дела 😀

Первое, что нужно, это преобразовать строку в URL:

Swift Combine

Так как процесс конвертации всегда возвращает опционал, сразу обрабатываем его с помощью guard. Напомню функция работает до первого return, поэтому если извлечение опционала даст nil, то на этом выполнении функции getCurrencyData завершится.

Для скачивания json-файла используем метод URLSession.shared.dataTaskPublisher с инициализатором for:

Swift Combine
  • URLSession - это объект, который координирует выполнение заданий по передаче данных;
  • shared - это singleton, который создан по умолчанию для выполнения задач по скачивания данных с заданного URL с использованием упрошенного синтаксиса.
  • dataTaskPublisher - собственно сам метод, который не просто скачивает данные, но и создаёт publisher. Метод dataTaskPublisher автоматически ставиться в global() очередь и выполняется в фоновом потоке.

Метод dataTaskPublisher по результатам скачивания возвращает два значения: данные и ответ сервера:

Swift Combine

Эти данные мы и будем обрабатывать далее с помощью методов применяемых к нашему publisher'у.

Передаём параметру for переменную url:

Swift Combine

Итак, мы создали publisher'у, темперь будем применять к нему операторы. Напомню, каждый оператор как бы создает нового дочернего publisher'а.

Итак, мы создали publisher, темперь будем применять к нему операторы. Напомню, каждый оператор как бы создает нового дочернего publisher'а.

.receive:

выбираем инициализатор с параметром on и задаём очередь, в которой будет выполняться задача (main, так как мы будем использовать полученные данные для обновления интерфейса, а всё что обновляет интерфейс должно быть в главном потоке)

Swift Combine

.tryMap:

Далее выбираем метод tryMap с инициализатором _ transform:

Swift Combine
Swift Combine

Что делает этот метод:

  • он принимает данные и ответ сервера (то есть URLSession.DataTaskPublisher.Output) и возвращает задаваемый нами тип данных;
  • метод помечен как throws означает, что он может выдать ошибку, собвственно этим tryMap и отличается от просто map, про который мы говорили в обьяснении, что такое операторы;
  • также мы видим, что tryMap не имеет обработчика выполнения и возвращает данные. На принтскрине выше мы видим возвращаемое значение → T, что является «заглушкой» и мы сами должны назначить возвращаемый тип данных, и так как получаемые данные еще не декодированы из JSON, то мы указываем просто Data.

Поехали! Нажимает Enter на функцию и она превращается в замыкание. Далее мы должны дать собственные имена получаемым параметрам и возвращаемому значению, назовём их просто: data и response, а возвращаемому значению назначим тип Data :

Swift Combine

Далее, в теле замыкания с помощью guard проверим значение ответа сервера и если он не успешный, то выведем информационное сообщение в консоль и выдадим ошибку, если ответ успешный метод возвращает только данные:

Swift Combine

Для того, чтобы проверить ответ сервера нам нужно сначала преобразовать наш ответ в тип HTTPURLResponse. URLResponse - это родительский класс типа HTTPURLResponse, поэтому для преобразования можно использовать оператор as. Значение успешного ответа должно быть в диапазоне от 200 до 299, подробнее про коды состояния HTTP можно почитатьтут.

Мы обработали URLResponse и ошибки, осталось обработать сами данные.

.decode

Теперь нужно декодировать данные из json формата в созданный нами тип CurrencyModel. Для этого используем метод .decode:

Swift Combine

В качестве типа, который мы хотим получить на выходе, мы указываем созданный нами тип CurrencyModel, а в качестве декодера - указываем JSONDecoder().

.sink

«sink» - переводиться как приём данных. Этот метод создаёт подписчика и запрос на неограниченное количество обновлений значения publisher`а, получает значения и предлагает их обработать задаваемым пользователем замыканием (обработчиком выполнения или Completion Handler).

Важно также отметить, что метод .sink имеет возвращаемое значение типа AnyCancellable.

Что такое AnyCancellable:

Swift Combine

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

Инициализируем оператор:

Swift Combine

Оператор .sink имеет два параметра (оба обработчики выполнения - Completion Handlers):

  • receiveCompletion - замыкание, которое принимает на вход сигнал о завершении отправки данных в виде отчета, успешна она прошла или нет. Вы должны обработать результат. Отчет (Subscribers.Completion) - это перечисление, которое может иметь только два значения: finished и failure;
  • receiveValue - замыкание, которое принимает на вход передаваемые данные, а вы должны их применить в своей программе. Если вы не примените внутри замыкания данные, то подписка будет автоматически отменена.

Звучит сложнее, чем оно есть на самом деле, код получается совсем простой:

Swift Combine

Так как (Subscribers.Completion) - это перечисление, обрабатываем его с помощью switch и просто указываем вывести в консоль информацию о результате выполнения.

Ну а с самими данными вы знаете, что делать: добавить их в наше свойство currencyInfo, которое и будет хранить данные о курсе валют:

Swift Combine

Полностью код оператора sink будет выглядеть так:

Swift Combine

Давайте еще раз проговорим, что сделал оператор sink:

  • подписался на publisher'а типа Publisher.tryMap.decode, то есть на дочернего publisher'а от исходного URLSession.shared.dataTaskPublisher,
  • получил на вход сигнал о завершении отправки данных,
  • обработал сигнал о завершении отправки данных,
  • получил на вход передаваемые данные (уже декодированные в формате CurrencyModel),
  • обработал данные, то есть добавил в свойство currencyInfo,
  • создал подписчика, а именно экземпляр класса AnyCancellable, который выполняет протокол Subscriber,
  • создал запрос на неограниченное количество обновлений.
Swift Combine

Если вы правильно разобрались в коде выше, то у вас должен был остаться только один вопрос: .sink создает подписчика и куда он девается? А вот и ответ:

.store

Этот оператор как раз и сохраняет нашего подписчика в переменную. Для этого нужно создать сначала эту переменную, единственное условие, она дожна представлять из себя множество (Set):

Swift Combine

Далее сохраняем подписчика в наше множество подписчиков:

Swift Combine

Ниже приводим полный код получившейся функции getCurrencyData, если вы где-то запутались:

Swift Combine

Остаётся только передать её в инициализатор класса ViewModel:

Swift Combine

3. View

Чтобы завершить приложение, давайте выведем на экран курс доллара:

Swift Combine

Если вдруг не поняли цепочку:

  • vm - это наша ViewModel,
  • currenveInfo - это её единственное свойство, и оно массив,
  • first - это мы обращаемся к первому (и единсвенному) элементу массива, который имеет тип CurrencyModel, который мы создавали в начале,
  • valute - это свойство типа CurrencyModel, и оно словарь,
  • ["USD"] - это мы обращаемся к элементу словаря по ключу ["USD"], а элементы словаря имеют тип Valute, которые содержат уже информацию а валюте,
  • value - это текущее значение курса,
  • description - это способ преобразовать Double в String
Swift Combine

Получаем на экране приложения значение курса:

Swift Combine

Мы рассмотрели с вами в первом приближении фреймворк Combine. Надеюсь наша статья и пример добавили вам информации, и время на чтение было потрачено продуктивно. Combine конечно намного шире приведенного нами примера, он позволяет создавать свои собственные publisher'ы и subscriber'ы, задавать параметры подписок, частоту получения данных и т.д. Но: на самом деле это фреймворк уже устарел 🤷🏻‍♂️, в 2021 был представлен Structured concurrency, который намного нагляднее, функциональнее и удобнее. Combine, конечно, нужно знать, но погружать в детали я бы советовал только в случае конкретных задач, конкретной работы.

Если понравилась статья - ставьте лайк, делитесь с друзьями! Кстати, у нас есть телеграмм-канал, Дзен и группа ВК, будем благодарны за подписку.

Начать дискуссию