как зарегистрироваться в combine приложении
MVVM на основе Combine в UIKit и SwiftUI приложениях для UIKit разработчиков
Код находится на Github.
Пользовательский интерфейс ( UI ) будет содержать всего 2 UI элемента: текстовое поле для ввода города и метку для отображения температуры. Текстовое поле для ввода города — это активный ВХОД ( Input ), а отображающая температуру метка — пассивный ВЫХОД ( Output ).
Роль View Model в архитектуре MVVM состоит в том, что она берет ВХОД(Ы) с View (или ViewController в UIKit ), реализует бизнес-логику приложения и передаёт ВЫХОДЫ назад в View (или ViewController в UIKit ), возможно, представляя эти данные в нужном формате.
Создать View Model с помощью Combine независимо от того, какая бизнес-логика — синхронная или асинхронная — очень просто, если использовать ObservableObject класс с его @Published свойствами.
Модель данных и API сервиса OpenWeatherMap
Хотя сервис OpenWeatherMap позволяет выбирать очень обширную информацию о погоде, Модель интересующих нас данных будет очень простой, она представляет собой детальную информацию WeatherDetail о текущей погоде в выбранном городе и находится в файле Model.swift:
API для сервиса OpenWeatherMap мы разместим в файле WeatherAPI.swift. Центральной его частью будет метод выборки детальной информации о погоде WeatherDetail в городе city :
Создание View Model
Как мы будем это делать?
Перейти к нужному издателю fetchWeather (for city: String) в Combine нам поможет оператор flatMap :
Оператор flatMap создает нового «издателя» на основе данных, полученных от предыдущего «издателя».
Далее мы «подписываемся» на этого вновь полученного «издателя» с помощью очень простого «подписчика» assign (to: \.currentWeather, on: self) и присваиваем полученное от «издателя» значение @Published свойству currentWeather :
Мы только что создали в init( ) АСИНХРОННОГО «издателя» и «подписались» на него, в результате получив AnyCancellable «подписку».
Запоминается AnyCancellable «подписка» в переменной cancellableSet с помощью оператора store ( in: &self.cancellableSet) :
Оператор debounce используется для того, чтобы подождать, пока пользователь закончит набирать на клавиатуре необходимую информацию, и только после этого однократно выполнить ресурсозатратное задание.
Создание UI с помощью SwiftUI
Теперь, любые изменения @Published свойств будут приводить к «перерисовке» View :
Создание UI с помощью UIKit
В UIKit приложении у нас также будет два UI элемента: UITextField для ввода города и UILabel для отображения температуры. В ViewController у нас естественно будут Outlet для этих элементов:
Это позволит нам очень просто в viewDidLoad реализовать «ручную привязку» с помощью функции binding () :
На этом всё. Код находится на Github.
Готовимся к Combine
Полтора года назад я пел дифирамбы RxSwift. У меня ушло какое-то время, чтобы разобраться в нем, но когда это случилось, пути назад больше не было. Теперь у меня был самый лучший молоток в мире, и будь я проклят, если всё вокруг не казалось мне гвоздём.
На летней конференции WWDC Apple представила фреймворк Combine. На первый взгляд, он выглядит как немного более лучшая версия RxSwift. Прежде чем я смогу объяснить, что мне в нём нравится, а что нет, нам нужно понять, какую проблему призван решить Combine.
Реактивное программирование? И что?
Сообщество ReactiveX — частью которого является сообщество RxSwift — объясняет его суть так:
API для асинхронного программирования с наблюдаемыми потоками.
ReactiveX — это комбинация лучших идей из шаблонов проектирования «наблюдатель» (Observer) и «итератор» (Iterator), а также функционального программирования.
И что все же это на самом деле означает?
Основы
Чтобы действительно понять суть реактивного программирования, я считаю полезным разобраться, как мы к нему пришли. В этой статье я опишу, как можно взглянуть на существующие типы в любом современном ООП-языке, покрутить их и затем прийти к реактивному программированию.
В этом материале мы быстро углубимся в дебри, что не является абсолютно необходимым для понимания реактивного программирования.
Однако я считаю это любопытным академическим упражнением, особенно с точки зрения того, как сильно типизированные языки могут вести нас к новым открытиям.
Так что ждите моих следующих записей, если вам будут интересны новые подробности.
Enumerable
Известное мне «реактивное программирование» зародилось в языке, на котором я когда-то писал — C#. Предпосылка сама по себе довольно проста:
Что если вместо того, чтобы извлекать значения из enumerable, те сами будут отправлять вам значения?
Эту идею, «push вместо pull», лучше всего описали Брайан Бекман и Эрик Мейер. Первые 36 минут… я ничего не понял, но начиная примерно с 36-й минуты становится действительно интересно.
Короче, давайте переформулируем идею линейной группы объектов в Swift, а также объекта, который может итерировать по этой линейной группе. Сделать это можно с помощью определения этих фейковых Swift-протоколов:
Двойники
Давайте теперь всё это перевернём и сделаем двойников. Будем отправлять данные туда, откуда они приходили. И получать данные оттуда, откуда они уходили. Звучит абсурдно, но потерпите немного.
Двойник Enumerable
Начнём с Enumerable:
Знаю, это странно. Не уходите.
Двойник Enumerator
Здесь есть несколько проблем:
Мы можем сделать ещё кое-что: взгляните на сигнатуру завершения перебора:
Вероятно, со временем будет проиcходить что-то подобное:
А теперь давайте всё упростим и будем вызывать enumeratorIsDone лишь тогда, когда… всё будет действительно готово. Руководствуясь этой идеей, упростим код:
Приберёмся за собой
Поместим DualOfEnumerator сюда:
Вот какой двойник получится в итоге:
Хоть розой назови, хоть нет
Итак, еще один раз, вот что у нас получилось:
Давайте теперь поиграемся немного с именами.
Так гораздо лучше и понятнее.
А что насчёт имён типов? Они просто ужасны. Давайте поменяем их немного.
Ух ты
Эти два типа лежат в основе RxSwift и реактивного программирования.
Насчёт «фейковых» протоколов
Упомянутые мной выше два «фейковых» протокола на самом деле вовсе не фейковые. Это аналоги существующих типов в Swift:
Так о чём волноваться?
Так много в современной разработке — особенно разработке приложений — связано с асинхронностью. Пользователь внезапно нажал на кнопку. Пользователь внезапно выбрал вкладку в UISegmentControl. Пользователь внезапно выбрал вкладку в UITabBar. Веб-сокет внезапно дал нам новую информацию. Это скачивание внезапно — и наконец-то — завершилось. Эта фоновая задача внезапно завершилась. Этот список можно продолжать до бесконечности.
В современном мире CocoaTouch есть множество способов обработки подобных событий:
А теперь представьте, если был бы целый набор функций, позволяющий модифицировать эти потоки, преобразовывать их из одного типа в другой, извлекать информацию из Element’ов, или даже комбинировать их с другими потоками.
Внезапно в наших руках оказывается новый универсальный набор инструментов.
И вот, мы вернулись к началу:
API для асинхронного программирования с наблюдаемыми потоками.
Именно это делает RxSwift таким мощным средством. Как и Combine.
Что дальше?
Если вы хотите побольше прочитать об RxSwift на практике, то рекомендую мои пять статей, написанные в 2016-м. В них описывается создание простейшего CocoaTouch-приложения, с последующим поэтапным преобразованием в RxSwift.
В одной из следующих статей я расскажу, почему многие из методик, описанных в моём цикле статей для начинающих, не применимы в Combine, а также сравню Combine с RxSwift.
Combine: в чём суть?
Обсуждение Combine подразумевает и обсуждения основных различий между ним и RxSwift. Для меня их три:
Возможности RxCocoa
В одном из предыдущих постов я говорил, что RxSwift нечто большее, чем… RxSwift. Он предоставляет многочисленные возможности по использованию контролов из UIKit в типа-но-не-совсем подпроекте RxCocoa. Кроме того, RxSwiftCommunity пошли дальше и реализовали много привязок для ещё более укромных закоулков UIKit, а также некоторые другие классы CocoaTouch, которые пока не покрывает RxSwift и RxCocoa.
Поэтому очень легко можно получить Observable поток из, скажем, нажатия на UIButton. Еще раз приведу этот пример:
Давайте (наконец-то) все же поговорим о Combine
Combine очень похож на RxSwift. Как сказано в документации:
Фреймворк Combine предоставляет декларативный Swift API для обработки значений с течением времени.
Звучит знакомо: вспомним описание ReactiveX (родительского проекта для RxSwift):
API для асинхронного программирования с наблюдаемыми потоками.
В обоих случаях говорится об одном и том же. Просто в описании ReactiveX используются специфические термины. Его можно переформулировать так:
API для асинхронного программирования со значениями в течение времени.
Практически то же самое, как по мне.
То же самое, что и раньше
Когда я начал анализировать API, то стало сразу очевидно, что большинство известных мне типов из RxSwift имеют похожие варианты в Combine:
«Перерыв на какавушку»
Всё меняется, как только начинаешь углубляться в RxCocoa. Вспомните вышеприведённый пример, в котором мы хотели получить поток Observable, который представляет нажатия на UIButton? Вот он:
Чтобы сделать то же самое в Combine, требуется… гораздо больше работы.
Combine не предоставляет никаких возможностей по привязке к UIKit-объектам.
Это… просто нереальный облом.
Вот обычный способ получения UIControl.Event из UIControl с помощью Combine:
Тут… намного больше работы. Хотя бы вызов выглядит похоже:
Для сравнения, RxCocoa предоставляет приятное, вкусное какао в виде привязок к UIKit-объектам:
Для сравнения, мой ControlPublisher появился только… сейчас. Только из-за количества клиентов (ноль) и времени использования в реальном мире (практически ноль по сравнению с RxCocoa) мой код можно считать бесконечно опаснее.
Помощь сообщества?
Честно говоря, сообществу ничто не мешает создать свой open source «CombineCocoa», который бы заполнил пробел RxCocoa так же, как это сделало RxSwiftCommunity.
Тем не менее, я считаю это огромным минусом Combine. Я не хочу переписывать весь RxCocoa, только чтобы получить привязки к UIKit-объектам.
Если я решу сделать ставку на SwiftUI, то, полагаю, это избавит от проблемы отсутствия привязок. Даже моё маленькое приложение содержит кучу UI-кода. Выкинуть всё это только для того, чтобы запрыгнуть на поезд Combine, будет как минимум глупо, а то и опасно.
К слову, в статье из документации Receiving and Handling Events with Combine кратко описывается, как получать и обрабатывать события в Combine. Введение хорошее, в нем показывается, как извлекать значение из текстового поля и сохранять его в кастомном объекте модели. Документация также демонстрирует использование операторов для выполнения некоторых более продвинутых модификаций рассматриваемого потока.
Пример
Перейдём в конец документации, где приведён пример кода:
С этим у меня… куча проблем.
Уведомляю вас, что мне это не нравится
Больше всего вопросов у меня вызывают первые две строки:
NotificationCenter — это что-то вроде шины приложения (или даже системной шины), в которой много кто может забросить данные, или поймать кусочки пролетающей мимо информации. Это решение из категории всё-и-для-всех, как и было задумано создателями. И действительно есть много ситуаций, когда вам может быть необходимо узнать, скажем, что клавиатура была только что показана или скрыта. NotificationCenter — отличный способ распространения этого сообщения по всей системе.
Но для меня NotificationCenter — это код с душком. Бывают случаи (вроде получения уведомления про клавиатуру), когда NotificationCenter в самом деле является лучшим возможным решением проблемы. Но слишком часто для меня NotificationCenter — самое удобное решение. Действительно очень удобно закинуть что-то в NotificationCenter и забрать это что-то в другом месте приложения.
Кроме того, NotificationCenter «строко»-типизирован, то есть можно легко допустить ошибку, какое уведомление пытаешься опубликовать или слушать. Swift делает всё возможное, чтобы улучшить ситуацию, но под капотом до сих пор кроется все тот же NSString.
По поводу KVO
На платформе Apple уже давно есть популярный способ получения уведомлений об изменениях в разных частях кода: key-value observation (KVO). Apple описывает его так:
Это механизм, позволяющий объектам получать уведомления об изменениях в заданных свойствах других объектов.
Благодаря твиту Gui Rambo я заметил, что Apple добавила в Combine привязки к KVO. Это могло означать, что я смогу избавиться от многочисленных огорчений по поводу отсутствия в Combine аналога RxCocoa. Если я смогу использовать KVO, это, вероятно, устранит потребность в «CombineCocoa», если можно так выразиться.
Попробовал сообразить пример использования KVO для получения значения из UITextField и вывода его в консоль:
Выглядит неплохо, идем дальше?
Не так быстро, друзья.
UIKit, по большому счёту, не совместим с KVO.
А без поддержки KVO моя идея не сработает. Мои проверки это подтвердили: код ничего не выводит в консоль, когда я ввожу текст в поле.
Итак, мои надежды на избавление от потребности в UIKit-привязках были прекрасны, но недолги.
Очистка
Другая проблема Combine заключается в том, что пока что совершенно неясно, где и как нужно освобождать ресурсы в Cancellable объектах. Кажется, что мы должны хранить их в переменных экземпляра. Но не припоминаю, чтобы в официальной документации что-то говорилось об очистке.
В RxSwift есть ужасно-названный-но-невероятно-удобный DisposeBag. И не менее легко и создать CancelBag в Combine, но не совсем уверен в том, что в данном случае это самое лучшее решение.
В следующей статье мы поговорим об обработке ошибок в RxSwift и Combine, о достоинствах и недостатках обоих подходов.
Современный код для выполнения HTTP запросов в Swift 5 с помощью Combine и применение их в SwiftUI. Часть 1
Появление в Swift 5 нового фреймворка функционального реактивного программирования Combine в сочетании с уже существующими URLSession и Codable предоставляет вам все необходимые инструменты для самостоятельного написания очень компактного кода для выборки данных из интернета.
При обращении к такого рода сервисам могут возникать ошибки, например, связанные с тем, что вы задали неправильный ключ API-key или превысили допустимое количество запросов или еще что-то. Необходимо обрабатывать такого рода ошибки, иначе вы рискуете оставить пользователя в полном недоумении с пустым экраном. Поэтому надо уметь не только выбирать с помощью Combine данные из интернета, но и сообщать об ошибках, которые могут возникнуть при выборке, и управлять их появлением на экране.
Код приложения для данной статьи находится на Github.
Модель данных и API сервиса NewsAPI.org
То, какие статьи или источники информации мы хотим выбирать с сервера NewsAPI.org, будем указывать с помощью перечисления enum Endpoint:
Он возвращает нам «издателя» AnyPublisher со значением в виде массива источников информации [Source] и отсутствием ошибки Never (в случае ошибок возвращается пустой массив источников [ ] ).
Это позволит упростить предыдущие два метода:
В этой статье мы сосредоточимся на том, как сделать нашу Модель для SwiftUI внешним «источником истины» (source of truth).
Давайте сначала рассмотрим, как в SwiftUI должны функционировать полученные «издатели» на конкретном примере отображения различного рода статей:
Как мы будем это делать?
Для создания нового «издателя» на основе данных, полученных от предыдущего «издателя» в Combine используется оператор flatMap :
Далее мы «подписываемся» на этого вновь полученного «издателя» с помощью очень простого «подписчика» assign (to: \.articles, on: self) и присваиваем полученное от «издателя» значение @Published массиву articles :
Мы только что создали в init( ) АСИНХРОННОГО «издателя» и «подписались» на него, в результате получив AnyCancellable «подписку» и это легко проверить, если мы сохраним нашу «подписку» в константе let subscription :
Таким образом мы можем получать массивы статей для всех трех опций — «topHeadLines», «search» и «from category»:
. но для фиксированной и заданной по умолчанию поисковой строки searchString = «sports» (там, где она требуется):
Однако для опции «search» необходимо предоставить пользователю текстовое поле SearchView для ввода поисковой строки:
В результате пользователь сможем искать любые новости по набранной поисковой строке:
Для опции «from category» необходимо предоставить пользователю возможность выбрать категорию и начинаем мы с категории science :
Мы видим, как очень простая ObservableObject Модель, имеющая два управляемых пользователем @Published свойства — indexEndpoint и searchString — позволяет выбрать широкий спектр информации с сайта NewsAPI.org.
Список источников информации
Мы получим список источников информации для различных стран:
… и возможность поиска их по названию:
… а также детальную информацию о выбранном источнике: его имя, категорию, страну, краткое описание и ссылку на сайт:
Если кликнуть на ссылке, то попадем на сайт этого источника информации.
Для того, чтобы все это работало нужна предельно простая ObservableObject Модель, имеющая всего два управляемых пользователем @Published свойств — searchString и country :
В результате получим нужное нам View :
Вообще, если вы посмотрите на всё приложение NewsApp, то нигде не увидите, чтобы мы явно запрашивали выборку статей или источников информации с сайта NewsAPI.org. Мы управляем только @Published данными, а View Model делает свою работу: выбирает нужные нам статьи и источники информации.
Модель статьи Article содержит URL сопровождающего её изображения urlToImage :
На основании этого URL мы в дальнейшем должны получить само изображения UIImage с сайта NewsAPI.org.
Используем оператор flatMap и очень простого «подписчика» assign (to: \image, on: self) с целью присваивания полученного от «издателя» значения свойству @Published image :
«Издателя» AnyPublisher на основе url создаем в функции fetchImageErr (for url: URL?) :
Далее мы выполним следующие шаги, учитывая при этом все возможные ошибки (мы не будем идентифицировать ошибки, нам просто важно знать, что ошибка есть):
Напоследок мы упакуем все наши возможности показа данных с агрегатора новостей NewsAPI.org в TabView :
Отображение ошибок при выборке и декодировании JSON данных с сервера NewsAPI.org.
При обращении к серверу NewsAPI.org могут возникать ошибки, например, связанные с тем, что вы задали неправильный ключ API-key или, имея тариф разработчика, который ничего не стоит, превысили допустимое количество запросов или еще что-то. При этом сервер NewsAPI.org снабжает вас HTTP кодом и соответствующим сообщением:
Необходимо обрабатывать такого рода ошибки сервера. Иначе пользователь вашего приложения попадёт в ситуацию, когда вдруг ни с того, ни с сего, сервер NewsAPI.org перестанет обрабатывать какие-либо запросы, оставляя пользователя в полном недоумении с пустым экраном.
Затем мы должны установить для полученного «издателя» ТИП ошибки равный требуемому NewsError :
И ещё добавляем показ экстренного сообщения Alert для случая появления ошибки.
Например, если неверный API ключ:
… то мы получим такое сообщение:
Если лимит запросов исчерпан, то мы получим такое сообщение:
Как мы знаем, этот код очень легко использовать для получения конкретного «издателя», если исходными данными для url является Endpoint для агрегатора новостей NewsAPI.org или страна country источника информации, а на выходе требуются различные Модели — например, список статей или источников информации:
Заключение.
Если нужно учитывать ошибки, то код для Generic «издателя» немного усложнится, но все равно это будет очень простой код без каких-либо callbacks:
Полученные всевозможные «издатели» можно очень просто «заставить работать» в ObservableObject классах, которые с помощью своих @Published свойств управляют вашим UI, спроектированным с помощью SwiftUI. Эти классы обычно играют роль View Model, так как в них есть так называемые «входные» @Published свойства, соответствующие активным UI элементам (текстовым полям TextField, Stepper, Picker, переключатели Toggle и т.д.) и «выходные» @Published свойства, состоящие в основном из пассивных UI элементов (текстов Text, изображений Image, геометрических фигур Circle(), Rectangle() и т.д.
Этой идеей пронизано всё приложение для работы с агрегатором новостей NewsAPI.org, представленное в этой статье. Она оказалась достаточно универсальной и использовалась при разработке приложения для базы данных фильмов TMDb и агрегатора новостей Hacker News, о которых будет рассказано в следующих статьях.
Код приложения для данной статьи находится на Github.
1. Хочу обратить ваше внимание на то, что если вы пользуетесь симулятором для приложения, представленного в этой статье, то знай, что NavigationLink на симуляторе работает с ошибкой. Вы можете воспользоваться NavigationLink на симуляторе только 1 раз. Т.е. вы использовали ссылку, вернулись назад, кликаете на ту же ссылку — и ничего не происходит. До тех пор пока вы не воспользуетесь другой ссылкой, первая не заработает, зато вторая станет недоступной. Но такое наблюдается только на симуляторе, на реальном устройстве все работает нормально.
2. Некоторые источники информации все еще используют http вместо https для «картинок» своих статей. Если вы определённо хотите увидеть эти «картинки», но не можете контролировать источник их появления, то вам придётся настроить систему безопасности ATS ( App Transport Security) на получение этих http «картинок», но это, конечно, не самая хорошая идея. Можно воспользоваться более безопасными вариантами.