Взаимодействие с сервером через API в iOS на Swift 3. Часть 1
Данная статья является обновлением статьи Получение удаленных данных в iOS, написанной в ноябре 2015 с использованием Objective-C и потому морально устарешней. Сейчас же будет приведен код, переписанный на Swift 3 и iOS 10 (последней версией является Swift 4.1 и iOS 11, но мой компьютер их уже не поддерживает).
Краткая теория
Формат url
http заголовок
Браузер преобразует строку url в заголовок и тело запроса. Для http-запроса тело пустое, а заголовок представлен следующим образом
Cхема запроса на сервер
Сначала создается запрос (request), потом устанавливается соединение (connection), посылается запрос и приходит ответ (response).
Делегаты сессии
Все UI операции (связанные с пользовательским интерфейсом) выполняются в главном потоке. Нельзя просто взять и остановить этот поток, пока выполняется какая-то ресурсоемкая операция. Поэтому одним из решений этой проблемы было создание делегатов. Таким образом, операции становятся асинхронными, а главный поток выполняется без остановок. Когда же нужная операция будет выполнена, то будет вызван соответствующий метод делегата. Второе решение проблемы — создание нового потока выполнения.
Как и в оригинальной книге, мы используем делегат, чтобы было операции были разделены между методами более наглядно. Хотя через блоки код получается более компактным.
Описание видов делегатов сессии
Мы используем NSURLSessionDownloadDelegate и реализуем его метод URLSession:downloadTask:didFinishDownloadingToURL:. То есть по сути скачиваем данные с шуткой во временное хранилище, и, когда загрузка завершена, вызываем метод делегата для обработки.
Переход в главный поток
Загрузка данных во временное хранилище осуществляется не в главном потоке, но чтобы использовать эти данные для изменения UI мы перейдем в главный поток.
«Убегающее» замыкание (@escaping)
Так как в силу реализации кода, замыкание которое мы передаем в метод загрузки данных с url, переживет сам метод, то для Swift 3 необходимо явно обозначить его @escaping, а self сделать unowned, чтобы не происходило захвата и удержания ссылки self в этом замыкании. Но это уже нюансы реализации самого языка Swift, а не техонологии получения данных по API.
Переадресация (редиректы)
В некоторых случаях происходят редиректы. Например, если у нас имеется некоторый короткий url, то когда мы вводим его в поисковую строку браузера, браузер сначала идет на сервер, где этот короткий url расшифровывается и отправляется к нам, а затем уже по этому полному url мы переходим на целевой сервер. При необходимости мы можем контролировать эти редиректы с помощью NSURLSessionTaskDelegate, но по умолчанию NSURLSession сама справляется со всеми деталями.
Схема сериализации
Сериализация — это процесс перевода данных из одного вида хранения в другой, без потери содержания. Например, хранятся данные в двоичном виде, чтобы занимать меньше места, а при пересылке по сети их преобразуют в универсальный JSON (JavaScript Object Notation) формат, который уже мы расшифровываем и переводим в объекты нашей среды программирования.
Фигурные скобки обозначают словарь (dictionary), а объекты внутри словаря представлены парами ключ-значение.
API (Application Programming Interface)
В нашем случае API представлен адресом, откуда мы будет получать случайные шутки и форматов JSON ответа, который нам нужно разобрать в удобные для манипулирования структуры
А теперь практика
Весь проект, как и прошлый раз, реализован в коде, без использования storyboard. Весь код написан в 3х файлах: AppDelegate.swift, MainViewController.swift и HTTPCommunication.swift. AppDelegate.swift содержит общую настройку приложения. HTTPCommunication.swift осуществляет настройку соединения (запрос, сессия) и получение данных. В MainViewController.swift эти данные сериализуются для вывода, а также содержится код пользовательского интерфейса.
Создаем пустой проект. Для простоты пишем приложение только для iPhone. Удаляем ViewController.swift, Main.storyboard и в Info.plist также удаляем ссылку на storyboard, а именно строку Main storyboard file base name — String — Main.
По умолчанию App Transport Security в iOS блокирует загрузки из интернета по обычному http (не https), поэтому вносим изменения в Info.plist, как показано ниже. Для этого открываем Info.plist как source code, то и добавляем следующий код:
Мы, как и по умолчанию, запрещает произвольные загрузки: ключ NSAllowsArbitraryLoads в false. Но добавляем в виде исключения наш домен с шутками и все поддомены: значения ключа NSExceptionDomains.
Теперь в AppDelegate.swift переписываем application(_:didFinishLaunchingWithOptions:) следующим образом:
Создаем файл HTTPCommunication.swift. И пишем в нем следующий код.
Теперь распишем код данных функций.
Копируем код retrieveURL(_ url:, completionHandler:)
Копируем код func urlSession(_ session:, downloadTask:, didFinishDownloadingTo:)
Создаем файл MainViewController.swift и копируем следующий код, который создает необходимый интерфейс:
Разобрались с интерфейсом, теперь можно заполнять функционал.
Вот код retrieveRandomJokes()
Теперь запускаем приложение и получаем следующий результат.
Пока мы ждем получения шутки с сайта.
Наконец, шутка загружена и отображена.
В следующей статьи мы посмотрим на переписанную на swift вторую часть приложения, которая позволяет получать новые шутки, не перезапуская программу, а также голосовать за шутки.
Архитектура сетевого ядра в iOS-приложении на Swift 3. Часть 1
Для начала немного поясню, о чем пойдет речь в данной статье. Сейчас большинство мобильных приложений, на мой взгляд, являются клиент-серверными. Это означает, что они содержат в составе кода сетевое ядро, отвечающее за пересылку запросов и получение ответов от сервера. Причем речь вовсе не о сетевых библиотеках, которые берут на себя ответственность по «низкоуровневому» управлению запросами вроде пересылки REST-запросов, построения multipart-тела, работы с сокетами, веб-сокетами, и так далее. Речь идет о дополнительной обвязке, которая позволяет управлять запросами, ответами и данными состояния, характерными конкретно для вашего сервера. Именно в вариантах реализации этой обвязки и заключены основные проблемы сетевого уровня во многих мобильных проектах, с которыми мне приходилось работать.
Данная статья ставит целью привести один из вариантов архитектурного решения по построению сетевого ядра приложения, к которому я пришел после долгого времени работы с разными моделями и различными серверными API, и который на данный момент является наиболее оптимальным для задач, встречающихся мне в процессе работы над проектами. Надеюсь, этот вариант поможет вам разработать хорошо структурированное и легко расширяемое сетевое ядро, если вы начинаете новый проект или модифицируете существующий. Также буду рад услышать ваши советы и комментарии по улучшению кода и/или предложенной архитектуры. И да, статья из-за большого объема будет выпущена в двух частях.
Подробности под катом.
Клиент-серверное взаимодействие
В процессе работы приходится разбирать множество самых разных проектов, так или иначе взаимодействующих с серверами, построенными по REST-протоколу. И в большинстве этих проектов наблюдается картина из разряда «кто в лес, кто по дрова», поскольку сетевое ядро везде реализуется по-разному (впрочем, как и другие архитектурные части приложения). Особенно плохо дела обстоят, когда в одном проекте видна рука нескольких разработчиков, сменявших друг друга (да еще и в условиях сжатых сроков, как правило). В таких случаях нередко получается «ядро» довольно жуткого вида, чуть ли не обладающее собственным интеллектом. Попробуем уберечься от таких проблем с помощью предложенного подхода.
С чего начнем?
А в чем тут сложность?
И правда, пока все звучит довольно просто. Однако на деле эти 4 простых пункта содержат в себе дополнительные шаги, требующие дополнительных трудозатрат, и вот как раз тут начинается разнообразие в плане реализации этих самых промежуточных шагов. Немного расширим нашу последовательность, дополнив ее вопросами, возникающими в процессе работы:
Проектирование
Для решения всех приведенных выше вопросов нам понадобятся несколько сущностей:
Для наглядности я отразил весь процесс на схеме (изображение кликабельно):
Реализация ядра
Поехали. Создадим Playground и пойдем прямо по шагам из предыдущего раздела. Плюс, допустим, что в большинстве случаев мой сервер возвращает мне JSON. Какой парсер JSON использовать — это на ваше усмотрение, на момент написания статьи, опять-таки, адаптированных под третью версию языка библиотек еще не было, поэтому я использовал самописный парсер GJSON.
Информация об ошибке
Тут все довольно элементарно, класс с парой полей и инициализатором. Финализируем его, в рамках нашей задачи нам ни к чему расширения (конечно, в вашем проекте это может быть по-другому):
Информация об ответе
Чуть более подробный класс, принцип тот же. В данном случае он также соответствует ответу, возвращаемому сервером, это бинарные данные на вход, код ответа, опциональное сообщение + распарсенная ошибка:
Для удобства в вашем проекте, если вы работаете гарантированно с JSON в возвращаемых бинарных данных, то можно расширить этот класс через extension, например, добавив ему поле, возвращающее json-парсер (как пример):
Асинхронная сетевая задача
Вот это уже куда более интересная вещь. Назначение этого класса состоит в хранении всех отдаваемых данных, всех принимаемых данных и своего состояния (завершено/отменено). Также умеет отправлять себя в сеть и вызывать callback после завершения работы. При получении ответа от сервера записывает все полученные данные, в том числе делает попытку трансформации входных данных в JSON-объект, если это возможно, для упрощения последующей обработки. Также, поскольку в дальнейшем набор таких задач мы будем использовать внутри множества Set, нам понадобится реализовать пару системных протоколов.
Пул задач
Как и говорилось ранее, этот класс будет вести учет активных асинхронных задач, что может быть полезно при анализе или отладке. Бонусом он позволяет найти задачу по идентификатору и, например, отменить ее. Через пул задач производится отправка всех тасков в сеть (можно и без него, но тогда таск вам придется хранить где-то самостоятельно, что не очень удобно). Класс делаем синглтоном, разумеется.
Протокол
И завершающая часть, относящаяся непосредственно к сетевому ядру, — обработка ответов. Строгая типизация языка вносит свои коррективы. Мы хотим, чтобы наши классы-парсеры обрабатывали данные и отдавали нам определенный тип данных, а не какой-нибудь Any?. Для этих целей сделаем небольшой генерик-протокол, который в дальнейшем будем реализовывать:
Что дальше?
Собственно, на этом создание сетевого ядра завершено. Его можно спокойно переносить между проектами, только слегка подрихтовывая структуры информации об ответе и ошибке, а также адрес API cервера.
Во второй части данной статьи мы рассмотрим, как применить созданное сетевое ядро в проекте, чтобы максимально использовать преимущества предложенной архитектуры.
Perfect — REST сервер на Swift
Perfect — как заявляют создатели проекта — Идеальный веб-сервер и инструментарий для разработчиков, использующих Swift язык программирования для создания приложений и других служб REST. Понятно, что «Идеальный» — это не более чем игра слов, но вместе с тем, после знакомства с предлагаемым решеним начинаешь склоняться к тому, что толика правды в этом утверждении есть.
В «прессе» пробегали статьи о том, что на подходе новый язык программирования, который может стать промышленным стандартом с легкой подачи Apple. Язык, который базируется на продвигаемом в массы Swift. Как правило, статьи об этом вызывали больше вопросов, и еще больше раздражения у тех, кому надоело все переучивать (Swift сам по себе довольно быстро меняется). Однако, углубившись в изучение вопроса, становится понятным, что все намного лучше чем, кажется.
Perfect — это не новый язык, серверной разработки. Perfect это серверное окружение, которое позволяет создавать REST API сервисы используя исключительно Swift последней реализации (на момент написания статьи Swift 2.2) Там нет ничего, выходящего за рамки того, что приходится делать ежедневно клиентским разработчикам.
Что будем делать: Создадим страницу-визитку (заглушку), для демонстрации ее при обращении к серверу. Продемонстрируем возможности легкого создания REST API сервисов, которые будут отвечать на GET/POST запросы. Продемонстрируем механизм динамического формирования статических страниц сайта. Причем, делать будем все это на Swift.
Итак, отправным пунктом путешествия станет создание соответствующего окружения. К сожалению, путь который предстоит пройти не столь уж очевиден, и сопряжен с некоторым количеством весьма странных манипуляций — от создания Workspace до изменения схем в Xcode. Пошаговое руководство продемонстрировано в видео подготовленного авторами проекта. Описывать каждый шаг в статье — это скорее тема для хаба «переводы». Мне бы хотелось сосредоточится на практическом применении возможностей Perfect, которые отсутствуют в роликах, или поданы там в чудовищном загадочном виде. К слову сказать, некоторые ролики опубликованные авторами скорее вредны, чем полезны.
Для начала разберемся в понятиях. Perfect состоит из двух частей: Библиотеки сервера (PerfectLib), и запускаемого приложения с минималистическим интерфейсом (Perfect Server). Оба приложения имеют открытый код, и теоретически, Вы сами можете из изменить / допилить под свои нужны. Однако, я строго не рекомендую Вам это делать. Лично у меня постоянно возникают поползновения что-то улучшить. Но следует учитывать, что Perfect не адаптирован для использования совместно с Swift 3. А создатели языка заявляют, что Swift 3 не будет иметь поддержки «сверху в низ», а это значит, что после выхода 3-й версии языка Perfect гарантировано будет обновлен, и Вам придется полностью избавится от уже внесенных изменений, чтоб апнутся на новую версию Swift.
Если Вы еще не дошли до этапа «Hello Perfect!» — самое время это сделать, Скачать необходимое окружение можно здесь. (Часть ссылок на сайте проекта — битые)
Далее, создадим файлы index.html и template.html, и затем добавим их в наш рабочий проект. После добавления зайдем в Build Phases и добавим шаг «New Copy Files Phase»
В конечном итоге окно должно будет выглядеть так:
По большей части все эти действия рассмотрены в видеоролике со страницы проекта: www.youtube.com/watch?v=J441eJ40PH4 Однако, рассмотренный случай позволяет либо хостить Web страницы, либо использовать REST API. Мы постараемся объединить обе потребности в одну возможность.
Полностью замените код PerfectServerModuleInit приведенным ниже кодом:
public func PerfectServerModuleInit()
<
Routing.Handler.registerGlobally()
// Root index.html page
Routing.Routes[«*»] =
// Request for static pages
Routing.Routes[«GET», [«/index», «/list»]] =
Теперь необходимо добавить еще несколько классов.
import Foundation
import PerfectLib
class HelloHandler:RequestHandler
<
func handleRequest(request: WebRequest, response: WebResponse)
<
response.appendBodyString(«Hello World!\n»)
response.appendBodyString(«Hello Perfect!\n»)
response.appendBodyString(«Hello Swift Server!\n»)
response.requestCompletedCallback()
>
>
Класс HelloHandler не делает ничего полезного, и используется, в основном для проверки того, что сервер запущен и доступен. Вы видите, что ответ сервера сводится к добавлению строки в выходной буфер, и обратный вызов клиента (браузера или клиентского приложения).
import Foundation
import PerfectLib
class StaticPageHandler:RequestHandler
<
var staticPage = «index.html»
internal init(staticPage:String) <
self.staticPage = staticPage
>
func handleRequest(request: WebRequest, response: WebResponse)
<
let file = ContentPage().page(request.documentRoot, pageFile: self.staticPage)
Класс StaticPageHandler позволяет хостить статические страницы с указанными именами. «По-умолчанию» будет использована index.html, но, в принципе, это может быть любая другая страница добавленная в проект.
import Foundation
import PerfectLib
class HelpHandler:RequestHandler
<
func handleRequest(request: WebRequest, response: WebResponse)
<
let list = Routing.Routes.description.stringByReplacingString(«+h», withString: «»)
let html = ContentPage(title:»HELP», body:list).page(request.documentRoot)
response.appendBodyString(«\(html)»)
response.requestCompletedCallback()
>
>
Класс HelpHandler позволяет получить список команд, обрабатываемых сервером. Некоторые другие серверные окружения (к примеру, MS Framework 4.5.1) позволяют получить автодокументируемое REST API сервера. Это очень удобно для разработчиков мобильных приложений — не приходится дергать разработчиков сервера на предмет обслуживания / добавления команд.
Update: В следующей статье мы усовершенствовали механизм документирования API
import Foundation
import PerfectLib
class CarsJson:RequestHandler
<
func handleRequest(request: WebRequest, response: WebResponse)
<
let car1:[JSONKey: AnyObject] = [«Wheel»:4, «Color»:»Black»]
let car2:[JSONKey: AnyObject] = [«Wheel»:3, «Color»:[«mixColor»:0xf2f2f2]]
let cars = [car1, car2]
let restResponse = RESTResponse(data:cars)
response.appendBodyBytes(restResponse.array)
response.requestCompletedCallback()
>
>
Класс CarsJson демонстрирует работу GET/POST запросов со сложной структурой возвращаемых данных. Возвращаемые данные представлены объектом AnyObject.
public class ContentPage:NSObject
<
private var title = «»
private var body = «»
private var footer = «»
public init(title:String=»», body:String=»», footer:String=»Copyright (C) 2016 _MY_COMPANY_NAME_. All rights reserved.»)
<
self.title = title
self.body = body
self.footer = footer
>
var page = template
page = page.stringByReplacingString(«<
page = page.stringByReplacingString(«<
page = page.stringByReplacingString(«<