API для удаленной асинхронной выборки с помощью Apple Combine



    Combine — это функциональный реактивный Swift фреймворк, который недавно реализован для всех платформ Apple, включая Xcode 11. С помощью Combine очень легко обрабатывать последовательности асинхронно появляющихся во времени значений values. Он также позволяет упростить асинхронный код, отказавшись от делегирования и сложных вложенных callbacks.

    Но изучение самого фреймворка Combine на первых порах может показаться не таким уж простым. Дело в том, что основными «игроками» Combine являются такие абстрактные понятия, как «издатели» Publishers, «подписчики» Subscribers и операторы Operators, без которых не удастся продвинуться в понимании логики функционирования Combine. Однако благодаря тому, что Apple предоставляет разработчикам уже готовых «издателей», «подписчиков» и операторов, код, написанный с помощью Combine, оказывается очень компактным и хорошо читаемым.

    Вы увидите это на примере приложения, связанного с асинхронной выборкой информации о фильмах из очень популярной сейчас базы данных TMDb. Мы создадим  два различных приложения: UIKit и SwiftUI, и покажем, как с ними работает Combine.



    Надеюсь, эта статья облегчит Вам изучение Combine. Код для всех приложений, разработанных в этой статье, можно найти на Github.

    В Combine есть несколько главных компонент:

    «Издатель» Publisher.




    «Издатели» Publishers — это ТИПЫ, которые доставляют значения values всем, кто этим интересуется. Концепция «издателя» Publisher реализована в Combine в виде протокола  protocol, а не конкретного ТИПА. У протокола Publisher есть ассоциированные Generic ТИПЫ для выходного значения Output и ошибки Failure.
    «Издатель», который никогда не публикует ошибку, использует ТИП Never для ошибки Failure.





    Apple предоставляет в распоряжение разработчиков конкретные реализации уже готовых «издателей»: Just, Future, Empty, Deferred, Sequence, @Published и т.д. Добавлены «издатели» также к классам Foundation: URLSession, <NotificationCenter, Timer.

    «Подписчик» Subscriber.




    Это также протокол protocol, который обеспечивает интерфейс для «подписки» на значения values от «издателя». У него есть ассоциированные Generic ТИПЫ для входного значения Input и ошибки Failure. Очевидно, что ТИПЫ «издателя» Publisher и «подписчика» Subscriber должны совпадать.



    Для любого «издателя» Publisher есть два встроенных «подписчика» Subscribers: sink и assign:



    «Подписчик» sink основан на двух замыканиях: одно замыкание, receiveValue, выполняется тогда, когда вы получаете значения values, второе замыкание, receiveCompletion, выполняется при завершении «публикации» (нормальным образом или с ошибкой).



    «Подписчик» assign, продвигает каждое полученное значение value, по направлению к заданному key Path.

    «Подписка» Subscription.




    Сначала «издатель» Publisher создает и поставляет «подписку» Subscription «подписчику» Subscriber через его метод receive (subscription:):



    После этого, Subscription может посылать свои значения values «подписчикам» Subscribers с помощью двух методов:



    Если вы завершили работать с «подпиской» Subscription, то можно вызвать её cancel ( ) метод:



    «Субъект» Subject.




    Это протокол protocol, который обеспечивает интерфейс для обоих клиентов, как для «издателя», так и для «подписчика».  По существу, «субъект» Subject - это «издатель»  Publisher, который может принимать входное значение Input и который вы можете использовать для того, чтобы «впрыскивать» значения values в поток (stream) путем вызова метода send(). Это может быть полезно при адаптации существующего императивного кода в Combine Модели.

    Оператор Operator.




    С помощью оператора вы можете создать нового «издателя» Publisher из другого «издателя» Publisher путем преобразования, фильтрации и даже путем комбинации значений values из множества предыдущих upstream «издателей» Publishers.



    Вы видите здесь множество знакомых имен операторов: compactMap, map, filter, dropFirst, append.

    Встроенные в Foundation «издатели» Publishers.


    Apple также предоставляет разработчикам несколько уже встроенных функциональных возможностей Combine во фреймворке Foundation<, то есть «издателей» Publishers, для таких задач, как выборка данных с помощью URLSession, работа с уведомлениями с помощью Notification, таймер Timer и наблюдение за свойствами на основе KVO. Эта встроенная совместимость действительно поможет нам интегрировать фреймворк Combine в наш текущий проект.
    Чтобы узнать больше об этом, можно посмотреть статью «The ultimate Combine framework tutorial in Swift».

    Что мы научимся делать с помощью Combine?


    В этой статье мы научимся применять фреймворк Combine для выборки данных о фильмах с сайта TMDb. Вот что мы будем вместе изучать:

    • Применение «издателя» Future для создания замыкания с Promise для единственного значения: либо value, либо ошибки.
    • Применение «издателя» URLSession.datataskPublisher для «подписки» на данные data, публикуемые заданным URL.
    • Применение оператора tryMap для преобразования данных data с помощью другого «издателя» Publisher.
    • Применение оператора decode для преобразования данных data в Decodable объект и опубликование его для передачи в последующие элементы цепочки.
    • Применение оператора sink для «подписки» на «издателя» Publisher с помощью замыканий.
    • Применение оператора assign для «подписки» на «издателя» Publisher и присвоения поставляемого им значения value заданному key Path.

    Начальный проект


    Прежде, чем мы начнем, мы должны должны зарегистрироваться, чтобы получить API ключ на сайте TMDb. Вам нужно также загрузить начальный проект из репозитория GitHub.
    Убедитесь, что вы разместили свой API ключ в классе class MovieStore в константе let apiKey.



    Вот основные строительные блоки, из которых мы создадим наш проект:

    • 1. Внутри файла Movie.swift находятся Модели, которые мы будем использовать в нашем проекте. Корневая структура struct MoviesResponse реализует протокол Decodable, и мы воспользуемся этим при декодировании JSON данных в Модель. В структуре MoviesResponse есть свойство results, которое также реализует протокол Decodable и представляет собой коллекцию фильмов [Movie]. Именно она нас и интересует:



    • 2. Перечисление enum MovieStoreAPIError реализует протокол Error. Наш API будет использовать это перечисление для представления разнообразного рода ошибок: ошибок получения URL urlError,  ошибок  декодирования decodingError и ошибок выборки данных responseError.



    • 3. В нашем API есть протокол MovieService с единственным методом fetchMovies (from endpoint: Endpoint), который выбирает фильмы [Movie] на основе параметра endpoint. Сам по себе Endpoint - это перечисление enum, которое представляет endpoint для обращения к базе данных TMDb с целью выборки таких фильмов, как nowPlaying (последние), popular (популярные), topRated (топовые) и upcoming (скоро на экране).



    • 4.Класс MovieStore - это конкретный класс, который реализует протокол MovieService для выборки данных с сайта TMDb. Внутри этого класса мы реализуем метод fetchMovies (...), используя Combine.



    • 5. Класс MovieListViewController — это основной ViewController класс, в котором мы реализуем с помощью метода sink «подписку» на метод выборки фильмов fetchMovies (...), который возвращает «издателя» Future, а затем обновим таблицу TableView с помощью новых данных о фильмах movies, используя новый DiffableDataSourceSnapshot  API.

    Прежде, чем мы начнем, давайте изучим некоторые основные компоненты Combine, которые мы будем использовать для API удаленной выборки данных.

    «Подписываемся» на «издателя» с помощью sink и его замыканий.


    Наиболее простой способ «подписаться» на «издателя» Publisher — это использовать sink с его замыканиями, одно из которых будет выполняться всякий раз, когда мы получаем новое значение value, а другое - когда «издатель» закончит поставку значений values.



    Помните, что в Combine каждая «подписка» возвращает Cancellable, который будет удален как только мы покинем наш контекст. Чтобы поддерживать «подписку» более длительное время, например, для асинхронного получения значений values, нам нужно сохранить «подписку» в свойстве subscription1. Это позволило нам последовательно получить все значения values (7,8,3,4).

    Future асинхронно «публикует»  единственное значение: либо value, либо ошибку Failure.


    Во фреймворке Combine «издатель» Future можно использовать для асинхронного получения единственного значения ТИПА Result с помощью замыкания. У замыкания один параметр — Promise, который является функцией ТИПа (Result<Output, Failure>) -> Void.

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



    Мы создаём Future с успешным результатом ТИПА Int и ошибкой ТИПА Never. Внутри замыкания Future мы используем DispatchQueue.main.asyncAfter (… ), чтобы задержать выполнение кода на 2 секунды, и тем самым имитируем асинхронное поведение. Внутри замыкания возвращаем Promise с успешным результатом promise (.success(… )) в виде целого случайного значения Int в диапазоне между 0 и 100. Далее мы используем две подписки на futurecancellable и cancellable1 — и обе дают один и тот же результат, хотя внутри генерируется случайное число.
    Примечание 1. Следует отметить, что «издатель» Future в Combine имеет некоторые особенности поведения по сравнению с другими «издателями»:

    • «Издатель» Future всегда «публикует» ОДНО значение (value или ошибку) и на этом завершает свою работу.

    • «Издатель» Future является классом class (reference type) в отличие от других «издателей» которые преимущественно являются структурами struct (value type), и ему передается в виде параметра замыкание Promise, которое создается сразу же при инициализации экземпляра «издателя» Future. То есть замыкание Promise передается до того, как какой-нибудь «подписчик» subscriber вообще подпишется на экземпляр «издателя» Future. «Издатель» Future вообще не требует для своего функционирования «подписчика», как этого требуют все остальные обычные «издатели» Publishers. Именно поэтому в вышеприведенном коде печатается текст «Hello from inside the future!» только один раз.

    • «Издатель» Future является eager (нетерпеливым) «издателем» в отличие от большинства остальных lazy «издателей» («публикуют» только при наличие «подписки»). Лишь однажды замыкание «издателя» Future вызывает свой Promise, результат запоминается и затем поставляется текущему и будущим «подписчикам». Из вышеприведенного кода мы видим, что при повторной «подписке» sink к издателю future всегда выдается одно и то же «случайное» значение  (в данном случае 6, но может быть и другое, но всегда одно и то же), хотя в замыкании используется случайное Int значение.

    Такая логика «издателя» Future позволяет успешно использовать его для запоминания асинхронного ресурсо-затратного вычисляемого результата и не беспокоить «сервер» для последующих многократных «подписок». 

    Если вас такая логика «издателя» Future не устраивает и вы хотите, чтобы ваш Future вызывался lazy и каждый раз вы бы получали новые случайные Int значения, то вам следует «обернуть» Future в Deferred:



    Мы будем использовать Future классическим образом, как это предлагается в Combine, то есть как «разделяемого» вычисляемого асинхронного «издателя».

    Примечание 2. Нужно сделать еще одно замечание относительно «подписки» с помощью sink к асинхронному «Издателю». Метод sink возвращает AnyCancellable, который мы не запоминаем постоянно. Это означает, что Swift разрушит AnyCancellable к тому времени, когда вы покинете данный контекст (scope), что собственно и происходит на main thread. Таким образом, получается, что AnyCancellable оказывается разрушенным прежде, чем замыкание с Promise сможет стартовать на main thread. Когда AnyCancellable разрушается, то вызывается его метод cancel, который в этом случае аннулирует «подписку».  Именно поэтому мы запоминаем наши sink «подписки» к future в переменных cancellable< и cancellable1 или в Set<AnyCancellable> ( ).


    Использование Combine для выборки фильмов с сайта TMDb.


    Начнем с того, что вы откроете стартовый проект и перейдете к файлу MovieStore.swift и методу fetchMovies с пустой реализацией:



    С помощью метода fetchMovies мы можем выбирать различные фильмы, задавая определенные значения входному параметру endpoint ТИПА Endpoint. ТИП Endpoint — это перечисление enum, которое принимает значения nowPlaying (текущие), upcoming (скоро выйдут на экран), popular (популярные), topRated (топовые):



    Давайте начнем с инициализации Future  с callback замыканием. Полученное Future мы затем вернем.



    Внутри callback замыкания мы генерируем URL для соответствующего значения входного параметра endpoint с помощью функции generateURL (with endpoint:Endpoint):



    Если правильного URL сформировать не удалось, то возвращаем ошибку с помощь promise (.failure ( .urlError (… )), в противном случае идем дальше и реализуем «издателя» URLSession.dataTaskPublisher.

    Для «подписки» на данные с некоторого URL мы можем использовать встроенный в класс URLSession метод datataskPublisher, который получает URL как параметр и возвращает «издателя» Publisher с выходными данными Output кортежа (data: Data, response: URLResponse) и ошибкой URLError.



    Для преобразования одного «издателя» Publisher в другого «издателя» Publisher используем оператор tryMap. По сравнению с map, оператор tryMap может «выбрасывать» throws ошибку Error внутри замыкания, которое возвращает нам нового «издателя» Publisher.

    На следующем шаге мы будем использовать оператор tryMap для проверки кода statusСode http ответа response, чтобы убедиться, что его значение находится между 200 и 300. Если нет, то мы выбрасываем throws значение ошибки responseError перечисления enum MovieStoreAPIError. В противном случае (когда нет ошибок) мы просто возвращаем полученные данные data следующему в цепочке «издателю» Publisher.



    На следующем шаге будем использовать оператор decode, который декодирует выходные JSON данные предыдущего tryMap «издателя» в Модель MovieResponse< с помощью JSONDecoder.



    jsonDecoder настраиваем на определенный формат даты:



    Чтобы обработка выполнялась на main thread, мы будет использовать оператор receive(on:) и передадим ему в качестве входного параметра RunLoop.main.  Это позволит «подписчику» получить значение value на main потоке.



    Наконец мы добрались до конца нашей цепочки преобразований, и там мы используем sink для получения «подписки» subscription на сформированную «цепочку» «издателей» Publishers. Для инициализации экземпляра класса Sink нам понадобятся две вещи, хотя одна из них — необязательная:

    • замыкание receiveValue: . Будет вызываться всякий раз, когда «подписка» subscription получает новое значение value от «издателя» Publisher.

    • замыкание receiveCompletion: (Необязательное). Будет вызвано после того, как «издатель» Publisher закончит публикацию значения value, ему передается перечисление completion, которое мы можем использовать для проверки того, действительно ли «публикация» значений закончена или завершение произошло по причине возникновения ошибки.

    Внутри замыкания receiveValue, мы просто вызываем promise с вариантом .success и значением $0.results, которым в нашем случае является массив фильмов movies. Внутри замыкания receiveCompletion мы проверяем, есть ли у completion ошибка error, затем передаем соответствующую ошибку promise с вариантом .failure.



    Заметьте, что мы здесь собираем все ошибки, «выброшенные» на предыдущих этапах «цепочки издателей».

    Далее выполняем запоминание «подписки» subscription в свойстве  Set<AnyCancellable> ( ).
    Дело в том, что «подписка» subscription является Cancellable,  это такой протоколом, который уничтожает и очищает все после завершения функции fetchMovies. Чтобы гарантировать сохранение «подписки» subscription и после завершения этой функции, нам необходимо запомнить «подписку» subscription во внешней по отношению к функции fetchMovies переменной. В нашем случае мы используем свойство subscriptions, которое имеет ТИП  Set<AnyCancellable> ( ) и применяем метод .store (in: &self.subscriptions), который обеспечивает нам работоспособность «подписки» после того, как функции fetchMovies завершит свою работу.  



    На этом мы заканчиваем формирование метода fetchMovies выборки фильмов из базы данных TMDb с помощью фреймворка Combine. Метод fetchMovies в качестве входного параметра from принимает значение перечисления enum Endpoint, то есть какие конкретно фильмы нас интересуют: 

    .nowPlaying  — фильмы, которые сейчас идут на экране,
    .upcoming - фильмы, которые скоро выйдут на экран,
    .popular - популярные фильмы,
    .topRated  — топовые фильмы, то есть с очень высоким рейтингом.
    Давайте попробуем применить этот API к проектированию приложения с обычным UIKit пользовательским интерфейсов в виде таблицы Table View Controller:



    и к приложению, пользовательский интерфейс которого построен с помощью нового декларативного фреймворка SwiftUI:



    «Подписываемся» на фильмы movies из обычного View Controller.


    Мы перемещаемся в файл MovieListViewController.swift и в методе viewDidLoad вызываем метод fetchMovies.



    Внутри нашего метода fetchMovies мы используем разработанный ранее movieAPI и его метод fetchMovies с параметром .nowPlaying в качестве endpoint входного параметра from. То есть мы будем выбирать фильмы, которые в данный момент идут на экранах кинотеатров.



    Метод movieAPI.fetchMovies (from:.nowPlaying) возвращает «издателя» Future, на которого мы «подписываемся» с помощью sink, и снабжаем его двумя замыканиями. В замыкании receiveCompletion проверяем, есть ли ошибка error и выводим экстренное предупреждение пользователю alert с отображением сообщения об ошибке.



    В замыкании receiveValue мы вызываем метод generateSnapshot и передаем ему выбранные фильмы movies.



    Функция generateSnapshot генерирует новый NSDiffableDataSourceSnapshot, используя наши movies, и применяет полученный snapshot к diffableDataSource нашей таблицы.

    Запускаем приложение и смотрим, как UIKit работает вместе с «издателями» и «подписчиками» из фреймворка  Combine. Это очень простое приложение, не позволяющее настраиваться на различные коллекции фильмов — демонстрируемые сейчас на экране, популярные, высоко-рейтинговые или те, которые собираются появиться на экране  в ближайшее время. Мы видим только те фильмы, которые собираются выйти на экран (.upcoming). Конечно, такую настройку можно сделать, добавив любой UI элемент для задания значений перечисления Endpoint, например, с помощью Stepper или Segmented Control, а затем обновить пользовательский интерфейс. Это общеизвестно, но мы не будем этого делать  в приложении на основе UIKit, а оставим это новому декларативному фреймворку SwiftUI.
    Код для приложения на основе UIKit  можно найти на Github в папке CombineFetchAPICompleted-UIKit.

    Используем SwiftUI для отображения фильмов movies

    .
    Создаём новое приложение CombineFetchAPI-MY со SwiftUI интерфейсом с помощью меню File-> New -> Project и выбираем шаблон Single View App в разделе iOS:



    Затем указываем имя проекта и способ создания UI SwiftUI:



    Далее задаём местоположения проекта и копируем в новый проект файл Модели Movie.swift и размещаем его в папке Model, необходимые для взаимодействия с TMDb файлы MovieStore.swiftMovieStoreAPIError.swift и MovieService.swift, и размещаем их соответственно в папках MovieService и Protocol:



    В SwiftUI требуется, чтобы Модель была Codable, если мы собираемся наполнять её JSON данными, и Identifiable, если мы хотим облегчить себе отображение списка фильмов [Movie] в виде списка List. Модели в SwiftUI не требуется быть Equatable и Hashable, как этого требовал UIKit API для UITableViewDiffableDataSource в предыдущем UIKit приложении. Поэтому убираем из структуры <struct Movie все методы, связанные с протоколами Equatable и Hashable:


    ........................... .


    Есть прекрасная статья Identifiable, в которой показано различие и сходство между Swift протоколами Identifiable, Hashable и Equatable.

    Пользовательский интерфейс, который мы создадим с помощью SwiftUI, основан на том, что мы меняем endpoint при выборке данных и получаем нужную нам коллекцию фильмов, которая представляется в виде списка:



    Также, как и в случае с UIKit, выборка данных производится с помощью функции movieAPI.fetchMovies (from endpoint: Endpoint), которая получает нужный endpoint и возвращает «издателя» Future<[Movie, MovieStoreAPIError]>. Если мы взглянем на перечисление Endpoint, то увидим, что мы можем инициализировать нужный вариант case в перечислении Endpoint и соответственно нужную коллекцию фильмов с помощью индекса index:



    Следовательно, для получения нужной нам коллекции фильмов movies, достаточно задать соответствующий индекс indexEndpoint перечисления Endpoint. Давайте сделаем это в View Model, которая в нашем случае будет классом MoviesViewModel, реализующем протокол ObservableObject. Добавим в наш проект новый файл MoviesViewModel.swift для нашей View Model:



    В этом очень простом классе у нас два @Published свойства: одно @Published var indexEndpoint: Int — входное, другое @Published var movies: [Movie] - выходное. Как только мы поставили @Published перед свойством indexEndpoint, мы  можем начать использовать его и как простое свойство indexEndpoint, и как издателя $indexEndpoint.
    При инициализации экземпляра нашего класса MoviesViewModel мы должны протянуть цепочку от входного «издателя» $indexEndpoint до выходного «издателя» ТИПА AnyPublisher<[Movie], Never>, который мы получаем с помощью уже известной нам функции movieAPI.fetchMovies (from: Endpoint (index: indexPoint)) и оператора flatMap.



    Далее мы «подписываемся» на этого вновь полученного «издателя» с помощью очень простого «подписчика» assing (to: \.movies, on: self) и присваиваем полученное от «издателя» значение выходному массиву movies.  Мы можем применять «подписку» assing (to: \.movies, on: self)  только в том случае, если «издатель» не выбрасывает ошибку, то есть имеет ТИП ошибки Never. Как этого добиться? С помощью оператора replaceError(with: [ ]), который заменит любые ошибки на пустой массив фильмов movies.

    То есть первый более простой вариант нашего SwiftUI приложения не будет выводить пользователю информацию о возможных ошибках.

    Теперь, когда у нас есть View Model для наших фильмов, приступим к созданию UI. Добавим в файл ContentView.swift нашу View Model как @EnvironmentObject переменную var moviesViewModel и заменим Text(«Hello, World!») на
    Text("\(moviesViewModel.indexEndpoint)"), который просто отображает индекс indexEndpoint варианта коллекции фильмов.



    По умолчанию в нашей View Model индекс коллекции indexEndpoint = 2, то есть мы при старте приложения должны увидеть фильмы, которые в ближайшее время выйдут на экран (Upcoming):



    Затем добавим UI элементы для управления тем, какую коллекцию фильмов мы хотим показывать. Это Stepper:



    … и Picker:



    Оба используют «издателя» $moviesViewModel.indexEndpoint нашей View Model, и с помощью одного из них (все равно какого) мы можем выбрать требуемую нам коллекцию фильмов:



    Затем добавляем список полученных фильмов с помощью List и ForEach и минимальными атрибутами самого фильма movie:



    Список отображаемых фильмов moviesViewModel.movies мы также берем из нашей View Model:



    Мы НЕ используем «издателя» $moviesViewModel.movies со знаком $, потому что не собираемся в этом списке фильмов ничего редактировать. Мы используем обычное свойство moviesViewModel.movies.

    Можно сделать список фильмов более интересным, если отображать в каждой строчке списка кинопостер соответствующего фильма, URL которого представлен в структуре Movie:



    Мы заимствуем эту возможность у Thomas Ricouard из его прекрасного проекта MovieSwiftUI.

    Также как и в случае загрузки фильмов movies, для изображения UIImage у нас появляется сервис ImageService, который реализует с помощью Combine метод fetchImage, возвращающий «издателя» AnyPublisher<UIImage?, Never>:



    … и final class ImageLoader: ObservableObject, реализующий протокол ObservableObject с @Published свойством image: UIImage?:



    Единственное требование, которое выдвигает протокол ObservableObject - это наличие свойства objectWillChange. SwiftUI использует это свойство для понимания того, что в экземпляре этого класса что-то изменилось, и как только это произошло, обновляет все Views, зависимые от экземпляра этого класса. Обычно компилятор автоматически создает свойство objectWillChange, а все @Published свойства также автоматически уведомляют его об этом. В случае каких-то экзотических ситуаций вы можете вручную создать objectWillChange и уведомлять его о происшедших изменениях. У нас именно такой случай. 
    В классе ImageLoader мы имеем единственное @Published свойство var image:UIImage?. Оно в данной остроумной реализации является одновременно и входным и выходным, При инициализации экземпляра класса ImageLoader, мы используем «издателя» $image и при «подписке» на него вызываем функцию loadImage(), которая загружает требуемое нам изображение poster нужного размера size и присваивает его @Published свойству var image:UIImage?. Об этих изменениях мы уведомляем objectWillChange.
    В таблице у нас может быть много таких изображений, что приводит к существенным временным затратам, поэтому мы используем кэширование экземпляров imageLoader класса ImageLoader:



    У нас есть специальное View для воспроизведения кинопостера MoviePosterImage:



    … и мы будем использовать его при отображении списка фильмов в нашем основном ContentView:





    Код для приложения на основе SwiftUI без отображения ошибок можно найти на Github в папке CombineFetchAPI-NOError.

    Отображение ошибок удаленной асинхронной выборки фильмов.


    До сих пор мы не использовали и не отображали ошибки, возникающие в процессе удаленной асинхронной выборки фильмов с сайта TMDb. Хотя используемая нами функция movieAPI.fetchMovies (from endpoint: Endpoint), позволяет это сделать, так как возвращает «издателя» Future<[Movie, MovieStoreAPIError]>.

    Для того, чтобы учесть ошибки, добавляем в нашу View Model еще одно @Published свойство moviesError: MovieStoreAPIError?, которое представляет ошибку. Это Optional свойство, его начальное значение равное nil, что соответствует отсутствию ошибки:



    Для того, чтобы получить эту ошибку moviesError, нам придется немного изменить инициализацию класса MoviesViewModel и использовать более сложного «подписчика» sink:



    Ошибку moviesError можно отобразить на UI, если она не равна nil …



    с помощью AlertView:



    Мы имитировали эту ошибку, просто убрав правильный API ключ:



    Код для приложения на основе SwiftUI с отображением ошибок можно найти на Github в папке CombineFetchAPI-Error.

    Если вы изначально планировали не обрабатывать ошибки, то можно обойтись без Future<[Movie],MovieStoreAPIError>, а вернуть обычный AnyPublisher<[Movie], Never> в методе fetchMoviesLight:



    Отсутствие ошибок (Never) позволяет нам использовать очень простого «подписчика» assign(to: \.movies, on: self):



    Все будет работать как и прежде:



    Заключение


    Применять фреймворк Combine для обработки последовательности асинхронно появляющихся во времени значений values очень просто и легко. Операторы, которые предлагает Combine, — мощные и гибкие. Combine позволяет нам избежать написание сложного асинхронного кода путем использования цепочки upstream «издателей» Publishers, применения операторов и встроенных «подписчиков»  Subscribers. Combine построен на более низком уровне, чем Foundation, во многих случаях не нуждается в Foundation и имеет потрясающее быстродействие. 



    SwiftUI также сильно завязан на Combine< благодаря своим @ObservableObject, @Binding и @EnvironmentObject.
    iOS разработчики давно ждали от Apple официального фреймворка такого рода и наконец в этом году это случилось. 

    Ссылки:

    Fetching Remote Async API with Apple Combine Framework
    try! Swift NYC 2019 — Getting Started with Combine
    «The ultimate Combine framework tutorial in Swift».

    Combine: Asynchronous Programming with Swift

    Introducing Combine — WWDC 2019 — Videos — Apple Developer. session 722
    (конспект сессии 722 «Введение в Combine» на русском языке)

    Combine in Practice — WWDC 2019 — Videos — Apple Developer. session 721
    (конспект сессии 721 «Практическое применение Combine» на русском языке)

    SwiftUI & Combine: Вместе лучше. Почему SwiftUI и Combine помогут вам создавать лучшие приложения.

    MovieSwiftUI.

    Visualize Combine Magic with SwiftUI Part 1 ()
    Visualize Combine Magic with SwiftUI – Part 2 (Operators, subscribing, and canceling in Combine)
    Visualize Combine Magic with SwiftUI Part 3 (See Combine Merge and Append in Action)
    Visualize Combine Magic with SwiftUI — Part 4

    Visualize Combine Magic with SwiftUI — Part 5

    Getting Started With the Combine Framework in Swift
    Transforming Operators in Swift Combine Framework: Map vs FlatMap vs SwitchToLatest
    Combine's Future
    Using Combine
    URLSession and the Combine framework
    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 7

      +7
      Товарищ, уберите все это кошмарное выделение жирно-синим, невозможно читать же!
        0
        Я правильно понял, они сделали свой RX с преферансом и куртизанками?
          0
          Да, это Rx. Кстати, очень хорошо сделан и с превосходным быстродействием, значительно превышающем быстродействие RxSwift.
            0
            Чем же вы нагружали RxSwift чтобы увидеть разницу в быстродействии?
                0
                Я все-таки ждал ваши эксперименты и выводы о быстродействии… Столкнулись ли лично вы с ситуацией, в которой RxSwift оказался столь медленным, что приходилось прибегать к другим способам решения задачи?
                  0
                  У меня нет опыта работы с RxSwift, но многие отмечают, что хотя Combine пока по функциональности уступает RxSwift, он существенно превосходит RxSwift по быстродействию. Да это и понятно, ведь RxSwift написан поверх Swift, а Combine — ниже Foundation.
                  Лично меня поразило быстродействие связки Combine+ SwiftUI в задаче выборки данных о фильмах и их кинопостерах, что относится к ресурсо- затратным задачам. В том примере, который приведен в этой статье, при переходе от одной коллекции фильмов ( от тех, которые сейчас на экране, к популярным), выборка данных происходит столь быстро, что даже нет необходимости в экране «Loading...». «Сто лет» занимаюсь этими задачами и Combine+ SwiftUI преподнесли мне приятный сюрприз.

        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

        Самое читаемое