02 марта 2023

➡️🍏 Сетевые запросы и REST API в iOS и Swift: протокольно-ориентированное программирование. Часть 1

iOS-developer, ИТ-переводчица, пишу статьи и гайды.
Из этой статьи вы узнаете, как отправлять сетевые запросы к удаленному REST API и как декодировать данные. Также обсудим, почему стоит предпочесть нативную библиотеку популярным AlamoFire и AFNetworking.
➡️🍏 Сетевые запросы и REST API в iOS и Swift: протокольно-ориентированное программирование. Часть 1
Данная статья является переводом. Автор: Matteo Manferdini. Ссылка на оригинальную статью.

В большинстве современных iOS-приложений обязательно встречается работа с сетью, например, взаимодействие с удаленной веб-службой, предоставляющей данные. И зачастую таким веб-сервисом является REST API, возвращающий данные в формате JSON.

Однако написание сетевого слоя iOS-приложения – непростая задача. При выполнении асинхронных сетевых вызовов вам необходимо объединить несколько функций Swift, SwiftUI и Foundation. Более того, многие части архитектуры вашего приложения должны взаимодействовать, что делает задачу сложнее, чем может показаться на первый взгляд.

Легко сказать: «Мне нужно получить какие-то данные из REST API». Но такое предложение скрывает массу сложностей. Многие разработчики просто собирают фрагменты кода, который они находят в Stack Overflow, или используют библиотеку по работе с сетью.

Но в работе с сетью есть множество скрытых подводных камней.

Однажды я работал над проектом, в котором случайным образом возникали странные ошибки. Приложение отображало список элементов, и пользователь мог добавить другие элементы. Но иногда при добавлении нового элемента приложение отвечало предупреждением о том, что этот объект уже существует на сервере. Поскольку это был новый элемент, такая ситуация явно была невозможна. И проблема была еще более странной. При этом предупреждение отображалось несколько раз.

После более глубокого исследования я обнаружил, что проблема была вызвана сетевым стеком приложения, которое имело неправильную архитектуру. Сетевые и обратные вызовы обрабатывались с помощью нотификаций, которых я обычно рекомендую избегать. Поскольку для одного и того же уведомления было множество прослушивателей обработчиков событий, сетевые вызовы для одного и того же элемента дублировались. Затем сервер отклонял дополнительные сетевые вызовы, в результате чего в приложении появлялось несколько предупреждений.

Архитектура – это тема, которую я часто освещаю в своих статьях, потому что это очень важная основа каждого приложения для iOS. Даже если вы грамотно подходите к работе с iOS SDK, если вы неправильно структурируете свой код, вы столкнетесь со всевозможными проблемами в работе приложения.

Глава 1: Интернет-технологии, лежащие в основе удаленных вызовов API

Работа с сетевыми запросами в приложениях для iOS не происходит в вакууме. Поскольку сетевые запросы к REST API проходят через интернет, они опираются на протоколы и стандарты, которые необходимо понимать, если ваше приложение получает данные через сеть.

Необходимые шаги для выполнения сетевых запросов к REST API из приложения iOS

Выполнение сетевых запросов в приложении для iOS – это не просто добавление дополнительного кода. При подключении к удаленному веб-сервису в iOS необходимо понимать, как работает множество элементов. В этой статье мы рассмотрим каждый аспект по отдельности.

Перечислим шаги, которые необходимо сделать для выполнения сетевого запроса в приложении iOS:

  • Понять, как работает удаленный веб-сервис.

В настоящее время в интернете существует множество общедоступных API. Каждый из них реализован по-своему и имеет собственную документацию. И, если вы работаете на компанию или клиента, вам, возможно, придется взаимодействовать с их собственным API. Современные веб-сервисы часто, но не всегда, основаны на архитектуре REST.

  • Понять, как работает протокол HTTP.

REST API основан на HTTP – протоколе связи, используемом во Всемирной паутине. Знание HTTP означает понимание структуры URL-адресов, действий, которые вы можете выполнить с помощью методов HTTP, параметров запроса и способов отправки или получения данных.

  • Получить список всех URL-адресов для выполнения необходимых запросов.

Каждый REST API предлагает серию URL-адресов для получения, создания, обновления и удаления данных на сервере. Они уникальны для каждого API. Их структура зависит от выбора, сделанного разработчиками, создавшими API. Если вам повезет, у выбранного вами API будет надлежащая документация. Однако чаще всего, особенно когда вы взаимодействуете с частным API, вам приходится разговаривать с разработчиками серверной части.

  • Узнать, как использовать систему загрузки URL в iOS SDK.

Фреймворк Foundation имеет надежный сетевой API, который удовлетворяет все сетевые потребности, особенно для протокола HTTP. Рабочей лошадкой этого API является класс URLSession. В интернете вы также можете найти альтернативные сетевые библиотеки, которые я не рекомендую использовать.

  • Выполнить сетевой запрос, чтобы получить данные, которые вам нужны в вашем приложении.

Собрав перечисленные выше элементы, вы можете написать код для выполнения удаленного вызова API и возврата некоторых данных. Данные поступают в разных форматах, таких как двоичные данные (для медиафайлов), JSON, XML, Markdown, HTML и другие.

Прежде чем вы сможете использовать такие данные в своем приложении, вы должны проанализировать их и преобразовать в свои типы моделей. Двоичные данные обычно напрямую преобразуются в соответствующие типы мультимедиа, которые вы найдете в iOS SDK. В случае структурированных данных, таких как JSON, синтаксический анализ также довольно прост. Для более сложных форматов, таких как XML, Markdown и HTML, вам, возможно, придется написать собственный кастомный анализатор.

  • Обработать асинхронный характер сетевых вызовов в Swift.

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

Сетевые запросы по своей природе медленные, поскольку серверу требуется время для ответа и передачи данных по сети. Это означает, что сетевой код должен работать в фоновом режиме, иначе ваше приложение не будет отвечать на запросы в течение длительного времени. Это по-разному влияет на то, как вы пишете свой код Swift, как вы обрабатываете обратные вызовы и как вы управляете памятью.

  • Наконец, используйте полученные данные в своем приложении.

Это тоже не так просто, как кажется. Сетевые запросы могут не выполняться по многим причинам, поэтому вам необходимо обрабатывать ошибки и отсутствующие данные. Вам также необходимо обновить пользовательский интерфейс вашего приложения и показать пользователю, что данные извлекаются по сети.

Больше полезных материалов вы найдете на нашем телеграм-канале «Библиотека мобильного разработчика»

REST API используют URL-адреса и протокол HTTP для определения ресурсов информации и передачи данных

Вы получаете доступ к любому REST API через интернет. Это означает, что различные ресурсы, предлагаемые API, определяются набором унифицированных указателей ресурсов или URL-адресов.

URL-адрес имеет разные компоненты, но в контексте REST API нас обычно интересуют только три:

  • Хост, который обычно является именем (или иногда IP-адресом), идентифицирующим другую конечную точку (сервер), к которой мы собираемся подключиться.
  • Путь, который идентифицирует ресурс, который мы ищем.
  • Опциональный запрос, в котором мы можем добавить дополнительные параметры, влияющие на возвращаемые данные (фильтрация, сортировка, разбиение по страницам и т.д.).

Однако URL-адреса – это лишь часть того, что вам нужно понять, чтобы взаимодействовать с REST API. Другая часть – это архитектура Representational State Transfer или REST.

REST – это тип архитектуры для веб-сервисов. Но нас, как разработчиков iOS, не волнует, как вся архитектура REST работает на стороне сервера. Все, что нас волнует, – это то, что мы видим в приложении для iOS.

Дополнительная литература: Полное руководство по URL-адресам в Swift и SwiftUI.

Выполнение вызовов REST API с использованием HTTP-запросов

REST работает по протоколу передачи гипертекста (HTTP) , который был создан для передачи веб-страниц через Интернет. Проще говоря, в HTTP мы отправляем запросы на сервер, который возвращает ответы.

HTTP – запрос обычно содержит:

  • URL-адрес, идентифицирующий ресурс, который нам нужен;
  • метод HTTP, указывающий действие, которое мы хотим выполнить;
  • опциональные параметры для сервера в виде HTTP-заголовков;
  • некоторые опциональные данные, которые мы могли бы отправить на сервер.
        GET /index.html HTTP/1.1
Host: www.example.com
Authorization: Basic YWxhZGRpbjpvcGVuc2VzYW1l
    

Большинство REST API используют только подмножество методов HTTP, чтобы указать, какие действия вы можете выполнять:

  • GET, чтобы получить ресурс
  • POST, чтобы создать или обновить ресурс
  • DELETE, чтобы удалить ресурс

Некоторые API также могут использовать методы HEAD, PUT или PATCH, хотя это зависит от навыков разработчика API. То, как они работают, зависит от конкретного API, который вы используете, поэтому вам всегда нужно проверять документацию, чтобы узнать, доступны ли они и что они делают.

Когда дело доходит до параметров, вы могли заметить, что у нас есть два варианта: либо строка запроса в URL-адресе, либо заголовки HTTP.

Итак, какой из них вы должны использовать?

Детали обычно зависят от API, но в целом:

  • Строка запроса предназначена для параметров, связанных с ресурсом, к которому вы обращаетесь.
  • Заголовки HTTP предназначены для параметров, связанных с самим запросом, например, заголовки аутентификации.

Наконец, в опциональный раздел данных запроса мы помещаем данные, которые хотим отправить в API при создании или изменении ресурса. Если вы просто получаете ресурс, вам не нужно добавлять какие-либо данные к вашему запросу. На самом деле спецификация HTTP гласит, что сервер может отклонить GET-запрос, содержащий данные.

Большинство REST API возвращают структурированные JSON и бинарные данные

Как я упоминал выше, в HTTP вы делаете запросы, а сервер отвечает. Ответ HTTP обычно содержит:

  • код состояния, представляющий собой некое число, которое говорит вам, выполнен ваш запрос или же произошла какая-то ошибка;
  • некоторые заголовки HTTP, указывающие дополнительную информацию об ответе;
  • данные, если вы их запрашивали.
        HTTP/1.1 200 OK
Date: Mon, 23 May 2005 22:38:34 GMT
Content-Type: text/html; charset=UTF-8
Content-Encoding: UTF-8
Content-Length: 138
Last-Modified: Wed, 08 Jan 2003 23:11:55 GMT
Server: Apache/1.3.3.7 (Unix) (Red-Hat/Linux)
ETag: "3f80f-1b6-3e1cb03b"
Accept-Ranges: bytes
Connection: close
    

Хотя REST API может использовать множество форматов, большинство API возвращают данные в формате Javascript Object Notation (JSON). JSON – это формат данных, созданный для того, чтобы быть легким, удобным для чтения людьми и простым для создания и анализа машинами.

Однако некоторые веб-службы могут использовать другие форматы. Распространенными форматами являются XML, Markdown или HTML. Если вы взаимодействуете с сервером Windows, вы можете получать данные в формате SOAP, что потребует от вас написания собственного синтаксического анализатора, поскольку он основан на XML.

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

Однако имейте в виду, что двоичные данные не приходят вместе с первоначальным ответом, чтобы сохранить легковесность последнего. Вы получаете URL-адреса в формате JSON данных, обычно указывающих на сеть доставки контента. Это означает, что вы будете обращаться к другому серверу с его собственными правилами, так что имейте это в виду.

Глава 2: Выполнение сетевых запросов в приложениях для iOS

Как и на других платформах, в iOS вы не взаимодействуете напрямую с сетевым оборудованием, а полагаетесь на более абстрактный слой, называемый системой загрузки URL. Некоторые разработчики полагаются на сторонние сетевые библиотеки, но это не обязательно и имеет ряд недостатков.

Следует ли вам использовать стороннюю библиотеку для работы в сети iOS, такую ​​как AlamoFire или AFNetworking?

Когда дело доходит до выполнения сетевых запросов в iOS, многие разработчики полагаются на сетевые библиотеки, такие как AlamoFire или AFNetworking. Следует ли вам использовать библиотеку вместо сетевого API iOS?

Мой короткий ответ: нет.

Я объясню почему – в следующем разделе. Но сначала я хочу опровергнуть некоторые причины, по которым люди вообще решили использовать такие библиотеки (по крайней мере, те, которые мне удалось найти):

  • Они проще в использовании.

Но так ли это на самом деле? Я очень скоро покажу, как можно выполнять сетевые вызовы в iOS с очень небольшим количеством кода и с использованием всего пары классов из среды Foundation. Да, документы Apple для iOS SDK немного лаконичны, но это проблема документации, а не API. У сторонних библиотек есть большая документация, часто задаваемые вопросы, руководства по переходу и множество вопросов по Stack Overflow, но они не выглядят проще в использовании.

  • Они асинхронны.

Не совсем понимаю это утверждение. Оно подразумевает собой следствие: использование системы загрузки URL-адресов в iOS является только синхронным. Надо уточнить, что это не соответствует действительности. Это было не так даже со старым классом NSURLConnection, который сейчас устарел. Поэтому я не понимаю, почему люди предлагают это как преимущество.

  • Вы пишете меньше кода.

Зависимость есть. Это может быть верно для простых сетевых запросов, но я бы также поспорил с этим. Кроме того, меньшее количество кода не обязательно означает меньшую сложность и не обязательно означает экономию времени. Подробнее ниже.

  • API AlamoFire использует цепочку методов.

Это приятная функция стала возможной благодаря Swift. Однако проблема здесь в том, что этот стиль кодирования, типичный для функционального реактивного программирования, вынуждает вас использовать определенную архитектуру, которая слишком сложна, чтобы обсуждать ее здесь.

Вы также должны решить, оправдывает ли одна эта функция использование большой сторонней библиотеки. Если все, что вам нужно, это цепочка методов, вы можете добавить свою реализацию поверх сетевого API iOS.

  • Происходит сокращение шаблонного кода в проекте.

Нет, не происходит. Шаблон просто заканчивается где-то еще. Причина в том, что принятие подхода библиотеки лишает вас большей гибкости, когда вы можете свободно выбирать свои абстракции. К концу статьи это станет ясно.

  • Вы можете изучить их и стать более квалифицированным программистом.

Вы также можете изучать их, не помещая в свои проекты. И, кстати, я не знаю о ваших предпочтениях, но я предпочитаю тратить время на изучение хорошо написанного материала или выступления на конференциях, а не просеивать тысячи строк недокументированного кода, пытаясь понять, как он работает. Например, попробуйте найти способ обойти этот файл запроса в библиотеке AlamoFire.

Почему стоит придерживаться iOS SDK, а не использовать стороннюю библиотеку

Теперь давайте посмотрим, почему я рекомендую не использовать сетевые библиотеки в ваших приложениях для iOS.

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

В конце концов, все сводится к одному из навыков, которые вы должны развивать как разработчик. Вы должны тщательно взвесить все «за» и «против» при принятии решения о том, следует ли вам использовать какую-либо библиотеку в проекте. Не делайте что-нибудь только потому, что кто-то говорит вам сделать это так. И это правило, конечно же, распространяется и на меня.

Мое самое большое опасение по поводу использования сетевой или любой сторонней библиотеки можно выразить одним предложением: вы добавляете в свой проект существенную внешнюю зависимость. А зависимости всегда сопряжены с затратами:

  • Вы не являетесь владельцем кода в библиотеке.

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

  • Обновления Swift и iOS могут сломать библиотеку.

Что произойдет, когда выйдут следующие версии iOS и Swift, а библиотека перестанет работать? Теперь вы зависите от внешней стороны. И это при условии, что библиотека хорошо поддерживается ее разработчиками. В противном случае вы останетесь один на один с проблемой.

Я работал над проектами, в которых релизы приходилось откладывать из-за некоторых библиотек, которые не собирались обновлять под новые версии iOS. Команде пришлось потратить много времени на удаление библиотек и переписывание всего кода, который их использовал.

  • Разработчики могут изменить работу библиотеки в любое время.

Да, Apple тоже меняет свои API. Но хотите ли вы зависеть более чем от одной третьей стороны, помимо Apple? По крайней мере, Apple дает вам время, отказываясь от API с предупреждениями в Xcode, удаляя их только в будущих выпусках iOS. С бесплатными библиотеками с открытым исходным кодом у вас нет никаких гарантий. А когда библиотека развивается, вам приходится выполнять переносы, которые вы не планировали.

  • Библиотеки определяют архитектурные решения в вашем проекте.

Это то, о чем я редко упоминаю, но для меня это важно. Я могу сказать вам по личному опыту, что добавление библиотеки в ваш проект часто означает, что вам предстоит разбираться с ее причудами. Вы всегда можете реорганизовать свой код, когда его структура больше не соответствует вашим потребностям. Что касается библиотеки, то решение за вас принял кто-то другой, и вам придется с этим жить.

  • Короткие пути хороши до тех пор, пока они не перестают быть таковыми.

Библиотеки таят в себе много подводных камней, потому что, будем честными, некоторые решения не очень продуманы.

AlamoFire имеет расширение для асинхронного запроса изображений через класс UIImageView. Все любят его, потому что он очень прост в использовании. За исключением того, что вы никогда не должны делать сетевые запросы от элементов пользовательского интерфейса. Это связывает ваш код с моделью вашего приложения и с сетевым SDK, чего следует избегать.

Вы пытались сделать это внутри ячеек табличного представления? Если нет, я избавлю вас от траты времени и расскажу, что происходит. Когда вы прокручиваете табличное представление и повторно используете ячейки, асинхронные сетевые обратные вызовы направляются не к тем объектам ячеек. Теперь вам нужно написать странный код в своих ячейках, чтобы защититься от этой проблемы.

И это еще хуже в SwiftUI, где одно изменение данных может обновить все дерево представления (view tree). Если вы делаете сетевые запросы из представлений, вы дублируете запросы и теряете обратные вызовы старых.

  • Библиотеки усложняют тестирование вашего кода.

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

Обработка HTTP-сессий через URLSession

После того как мы поговорили про работу протокола HTTP и REST API, пришло время выполнить сетевые запросы из нашего приложения.

Многим разработчикам сложно использовать сетевой API в iOS, потому что они не понимают, как он работает. Вот почему они часто полагаются на внешние библиотеки, которые «делают всю работу» за них. Но, как я уже сказал, это проблема документации, а не проблема SDK.

Надо сказать, что может быть сложно понять, как работает вся система загрузки URL-адресов в iOS. При этом сложность оправдана, поскольку SDK должен обрабатывать множество сценариев и протоколов. Но та часть, которую вам нужно знать, чтобы сделать сетевой запрос, довольно проста. Поняв это, вы сможете расширить свои знания.

Существует три фундаментальных типа, которые необходимо использовать для выполнения HTTP-запроса.

➡️🍏 Сетевые запросы и REST API в iOS и Swift: протокольно-ориентированное программирование. Часть 1

Это и все последующие изображения взяты отсюда.

Первым из трех является класс URLSession. Сеанс – основное понятие HTTP, представляющее собой последовательность запросов и ответов для получения связанных данных. Чтобы было легче понять это, представьте, как ваш браузер загружает веб-страницу.

В настоящее время веб-страницы состоят из множества частей. Первое, что запрашивает ваш браузер, – это исходный HTML-код страницы. Он содержит ссылки на многие другие ресурсы, такие как изображения, видео, таблицы стилей CSS, файлы javascript и т.д. Для отображения всей страницы браузер должен получать каждый ресурс отдельно и делает это за один сеанс.

Как следует из названия, вы используете класс URLSession для управления сеансами HTTP. Использование одного и того же экземпляра URLSession для выполнения нескольких запросов позволяет вам совместно использовать конфигурации или использовать такие технологии, как HTTP/2 Server Push, если они доступны.

Однако на практике использование общего экземпляра URLSession для нескольких запросов требует более сложных архитектурных концепций, которые выходят за рамки этой статьи. В большинстве приложений вы можете обойтись без использования общего синглтона или отдельных экземпляров.

Выполнение сетевого запроса с использованием классов URLRequest и URLSessionTask

Двумя другими необходимыми классами для выполнения сетевых запросов являются структура URLRequest и класс URLSessionTask.

Первая инкапсулирует метаданные одного запроса, включая URL-адрес, метод HTTP (GET, POST и т.д.), возможные заголовки HTTP и т.д. Для простых запросов GET вам вообще не нужно использовать этот тип.

Второй выполняет фактическую передачу данных. Обычно вы не используете этот класс напрямую, а используете один из его подклассов в зависимости от нужной вам задачи. Наиболее распространенным является URLSessionDataTask, который извлекает содержимое URL-адреса и возвращает его в виде значения Data.

Обычно вы не создаете экземпляр задачи данных самостоятельно. Класс URLSession делает это за вас, когда вы вызываете один из его методов dataTask. Но вы должны не забыть вызвать метод resume() для экземпляра задачи данных, иначе он не запустится.

Итак, вкратце, создание HTTP-запроса в iOS сводится к следующему:

  1. создание и настройка экземпляра URLSession;
  2. создание и установка значения URLRequest для ваших запросов, но только тогда, когда вам нужны какие-то определенные параметры. В противном случае вы можете использовать простое URL-значение.
  3. создание и запуск URLSessionDataTask, используя экземпляр URLSession, созданный на шаге 1.

После всех объяснений в статье для выполнения этих трех шагов потребуется удивительно мало кода:

        import Foundation

let url = URL(string: "example.com")!
let task = URLSession.shared.dataTask(with: url) { (data: Data?, response: URLResponse?, error: Error?) -> Void in
    // Parse the data in the response and use it
}
task.resume()
    

Вам действительно нужно использовать сетевую библиотеку?

Примечание. Новый API для многопоточности, представленный в Swift 5.5, вкупе с URLSession еще больше упрощает выполнение сетевых запросов. Читайте об этом здесь: Легкий параллелизм в Swift с Async/Await

Глава 3: Получение и декодирование данных

Как только вы поняли, какие части системы загрузки URL позволяют выполнять сетевые запросы, направленные на REST API, пришло время эффективно использовать их в своем коде. Хотя их использование просто и занимает всего несколько строк кода, расширение такого кода таит в себе множество подводных камней.

Создание типов моделей, соответствующих объектам REST API

В оставшейся части этой статьи мы создадим простое приложение для получения самого популярного вопроса о разработке iOS на Stack Overflow. Вы можете найти полный проект Xcode на GitHub.

➡️🍏 Сетевые запросы и REST API в iOS и Swift: протокольно-ориентированное программирование. Часть 1

Независимо от того, какой шаблон архитектурного проектирования вы используете в своем приложении, вам всегда нужны типы моделей для представления данных и бизнес-логики приложения. Это верно независимо от того, используете ли вы ванильную версию шаблона MVC, мою четырехуровневую версию для SwiftUI, мой шаблон Lotus MVC или любую другую производную MVC, такую ​​как MVVM или VIPER.

В приложениях, подключенных к сети, вы можете структурировать слой модели по своему усмотрению. Но когда ваши данные поступают из удаленного API, это накладывает некоторые ограничения на ваши типы. Удаленный API не заботится о том, как вы разрабатываете свое приложение. Вы должны уметь адаптироваться, поэтому лучше смотреть на данные, которые возвращает API, прежде чем определять свои типы.

Это данные JSON вопроса, поступающие из Stack Exchange API:

        {
    "items": [
    {
        "tags":[
        "ios",
        "swift",
        "xcode",
        "swiftui",
        "apple-sign-in"
        ],
        "owner":{
            "reputation":208,
            "profile_image":"https://lh4.googleusercontent.com/-imc9sXzFpBI/AAAAAAAAAAI/AAAAAAAAAAA/ACHi3rc6K1-_v3b-TD__YZFpmGK7ZMm--A/photo.jpg?sz=128",
            "display_name":"l b",
        },
        "view_count":3615,
        "answer_count":1,
        "score":17,
        "creation_date":1580748417,
        "question_id":60043628,
        "title":"Logout from Apple-Sign In"
    }
    ]
}
    

Я упростил приведенный выше код JSON, чтобы включить только интересующие нас поля. Полный ответ вы можете посмотреть в документации.

Из данных, возвращаемых API, видно, что владелец вопроса возвращается как отдельный объект. Также имеет смысл иметь отдельный тип в нашей модели.

        struct User {
    let name: String?
    let reputation: Int?
    let profileImageURL: URL?
    var profileImage: UIImage?
}

struct Question: Identifiable {
    let id: Int
    let score: Int
    let answerCount: Int
    let viewCount: Int
    let title: String
    let body: String?
    let date: Date
    let tags: [String]
    var owner: User?
}

struct Wrapper {
    let items: [Question]
}
    

Поскольку нам нужно будет отображать вопросы в таблице в нашем приложении, я пошел дальше и сделал тип Question соответствующим протоколу Identifiable.

Необязательные свойства любого типа предназначены для полей, которые, согласно документации, могут отсутствовать. По общему признанию, установить все три свойства User как nil – будет не лучшим решением в реальном приложении, но эта тема не является темой обсуждения в данной статье. Это детали реализации, которые зависят от конкретных приложений.

Обратите внимание, что у нас есть дополнительная структура Wrapper, поскольку из соображений согласованности данные каждого ответа упаковываются в другой объект JSON. Имейте в виду, что это всего лишь деталь API Stack Exchange. Но тем не менее мы должны позаботиться об этом.

Декодирование данных JSON, возвращаемых REST API, с использованием протоколов Codable

Теперь нам нужно преобразовать данные JSON, которые мы получаем из API, для наших типов моделей. Декодирование данных JSON в Swift долгое время было раздражающей задачей, и появилось множество различных подходов и библиотек. Некоторые из этих библиотек следовали подходу функционального программирования, иногда добавляя малопонятные функциональные концепции и операторы, такие как функторы и монады.

К счастью, в Swift 4 были представлены протоколы Codable , которые упрощают анализ JSON. Я долгое время был сторонником способа использования кода, при котором происходит преобразование данных для нужных типов моделей, поскольку он является частью бизнес-логики приложения. Поэтому я был очень рад видеть, что основная команда Swift придерживается того же подхода с Codable.

Все, что нам нужно сделать, это привести наши типы в соответствии с протоколом Decodable. И поскольку я назвал некоторые свойства в типах User и Question в соответствии с условными обозначениями Swift, нам также необходимо сопоставить для перечисления CodingKeys (для тех, кто его использует).

        struct User {
    let name: String?
    let reputation: Int?
    let profileImageURL: URL?
    var profileImage: UIImage?
}

extension User: Decodable {
    enum CodingKeys: String, CodingKey {
        case reputation
        case name = "display_name"
        case profileImageURL = "profile_image"
    }
}

struct Question: Identifiable {
    let id: Int
    let score: Int
    let answerCount: Int
    let viewCount: Int
    let title: String
    let body: String?
    let date: Date
    let tags: [String]
    var owner: User?
}

extension Question: Decodable {
    enum CodingKeys: String, CodingKey {
        case score, title, body, tags, owner
        case id = "question_id"
        case date = "creation_date"
        case answerCount = "answer_count"
        case viewCount = "view_count"
    }
}

struct Wrapper: Decodable {
    let items: [Question]
}
    

Недавно я прочитал статью, в которой рекомендуется отделять типы моделей от декодирования данных. Смысл в том, чтобы ваши типы моделей и бизнес-логика были независимыми от базовых данных. Хотя это правильное замечание, такой подход удваивает количество типов в вашем проекте и вводит много шаблонного кода.

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

Распространенный, но не оптимальный способ выполнения сетевых запросов в приложениях для iOS

Теперь, когда у нас есть типы моделей для представления данных, которые мы получаем, мы, наконец, можем получить некоторые данные из API Stack Exchange. Сначала я покажу вам распространенный подход, который я часто вижу и который вы, вероятно, узнаете, но он не является оптимальным. По общему признанию, я тоже так делал в прошлом, но теперь я знаю способ получше.

Как мы видели выше, делать HTTP-запросы с помощью URLSession несложно. Большинство разработчиков помещают этот код в класс сетевого менеджера/обработчика/контроллера.

Вызовы DispatchQueue.main.async нужны нам для того, чтобы вернуть выполнение кода в основной поток, поскольку обратный вызов задачи данных выполняется в фоновом потоке.

Если вы создаете экземпляр объекта сеанса, вы можете использовать очередь .main в качестве цели и избежать вызовов Dispatch. Но это также переносит декодирование JSON в основной поток. Для больших объемов данных лучше держать его в фоновом режиме.

В этой статье я не буду рассматривать обработку ошибок, которая является отдельной темой. Мы просто преобразуем любую ошибку в nil-значение, чего и так достаточно во многих приложениях, которые вы пишете.

Все идет нормально. Приведенный выше код работает, и вы уже можете использовать его для получения данных из API. Но вы уже знаете, что запросы – это не единственный тип данных, которые нам нужно получать из API. Даже в нашем небольшом примере приложения нам нужно сделать отдельный сетевой запрос для получения аватара владельца.

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

И тут начинаются проблемы.

Нам нужно обобщить наш NetworkManager, чтобы мы могли использовать его для выполнения всевозможных запросов. А поскольку между запросами меняется тип данных, которые мы запрашиваем, решение состоит в использовании дженериков Swift.

        struct Wrapper<T: Decodable>: Decodable {
    let items: [T]
}

class NetworkManager {
    func load<T>(url: URL, withCompletion completion: @escaping (T?) -> Void) {
        let task = URLSession.shared.dataTask(with: url) { (data, _, _,) -> Void in
            guard let data = data else {
                DispatchQueue.main.async { completion(nil) }
                return
            }
            switch T.self {
                case is UIImage.Type:
                DispatchQueue.main.async { completion(UIImage(data: data) as? T) }
                case is Question.Type:
                let wrapper = try? JSONDecoder().decode(Wrapper<Question>.self, from: data)
                DispatchQueue.main.async { completion(wrapper?.items[0] as? T) }
                case is [Question].Type:
                let wrapper = try? JSONDecoder().decode(Wrapper<Question>.self, from: data)
                DispatchQueue.main.async { completion(wrapper?.items as? T) }
                default: break
            }
        }
        task.resume()
    }
}
    

Поскольку то, как мы декодируем данные, зависит от их типа, теперь нам нужно переключить их, чтобы запустить соответствующий код. Длинные условные операторы, такие как switch в приведенном выше коде нарушают принцип открытого-закрытого SOLID.

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

Более того, при использовании такого метода это не NetworkManager сообщает вызывающей стороне, которая обычно является наблюдаемой obejct/view моделью, какой тип данных возвращает API. Именно вызывающая сторона должна указать правильный тип возвращаемого значения, сняв ответственность с сетевого уровня.

За исключением того, что вызывающий модуль ничего не решает. Все, что он может сделать, это угадать правильный тип, иначе сетевой вызов завершится ошибкой. Это связывает код моделей представлений с внутренней реализацией NetworkManager.

Некоторые проблемы могут быть решены только с помощью правильной архитектуры

Если вы являетесь разработчиком, знакомым с принципом открытости-закрытости, а многие, к сожалению, этот принцип не используют, возможно, вы знаете некоторые решения вышеуказанной проблемы. Распространенный способ – поместить общий код в дженерик метод, который можно использовать повторно, а затем разбить длинный условный оператор на отдельные.

Но и здесь это не работает.

        class NetworkManager {
    func load(url: URL, withCompletion completion: @escaping (Data?) -> Void) {
        let task = URLSession.shared.dataTask(with: url) { (data, _, _,) -> Void in
            DispatchQueue.main.async { completion(data) }
        }
        task.resume()
    }
    
    func loadImage(with url: URL, completion: @escaping (UIImage?) -> Void) {
        load(url: url) { data in
            if let data = data {
                completion(UIImage(data: data))
            } else {
                completion(nil)
            }
        }
    }
    
    func loadTopQuestions(completion: @escaping ([Question]?) -> Void) {
        let url = URL(string: "https://api.stackexchange.com/2.2/questions?order=desc&sort=votes&site=stackoverflow")!
        load(url: url) { data in
            guard let data = data else {
                completion(nil)
                return
            }
            let wrapper = try? JSONDecoder().decode(Wrapper<Question>.self, from: data)
            completion(wrapper?.items)
        }
    }
}
    

Даже если у нас есть дженерик метод load(url:withCompletion:), в других методах все еще много повторений кода. И я даже не включил часть кода для получения одного запроса. Представьте, что происходит, когда вы добавляете другие типы.

Проблема здесь не в коде, а в подходе. Мы продолжаем сталкиваться с различными проблемами, потому что пытаемся втиснуть весь код в класс NetworkManager. И это только потому, что кто-то где-то сказал нам, что так надо делать. Вместо этого правильным решением будет выбрать другую архитектуру для нашего сетевого уровня.

***

В следующей части мы рассмотрим:

  • протокольно-ориентированную архитектуру сетевого слоя;
  • выполнение сетевых запросов в реальном приложении SwiftUI.

Материалы по теме

Источники

Комментарии

ВАКАНСИИ

Добавить вакансию
Разработчик C++
Москва, по итогам собеседования

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ