Данная статья является обновлением статьи Получение удаленных данных в iOS, написанной в ноябре 2015 с использованием Objective-C и потому морально устарешней. Сейчас же будет приведен код, переписанный на Swift 3 и iOS 10 (последней версией является Swift 4.1 и iOS 11, но мой компьютер их уже не поддерживает).
Данный url из примера можно прочитать таким образом: http запрос с методом GET отправляется домену google.com, в корневую директорию /, с двумя параметрами q со значением Hello и safe со значением off.
Браузер преобразует строку url в заголовок и тело запроса. Для http-запроса тело пустое, а заголовок представлен следующим образом
Сначала создается запрос (request), потом устанавливается соединение (connection), посылается запрос и приходит ответ (response).
Все UI операции (связанные с пользовательским интерфейсом) выполняются в главном потоке. Нельзя просто взять и остановить этот поток, пока выполняется какая-то ресурсоемкая операция. Поэтому одним из решений этой проблемы было создание делегатов. Таким образом, операции становятся асинхронными, а главный поток выполняется без остановок. Когда же нужная операция будет выполнена, то будет вызван соответствующий метод делегата. Второе решение проблемы — создание нового потока выполнения.
Как и в оригинальной книге, мы используем делегат, чтобы было операции были разделены между методами более наглядно. Хотя через блоки код получается более компактным.
Мы используем NSURLSessionDownloadDelegate и реализуем его метод URLSession:downloadTask:didFinishDownloadingToURL:. То есть по сути скачиваем данные с шуткой во временное хранилище, и, когда загрузка завершена, вызываем метод делегата для обработки.
Загрузка данных во временное хранилище осуществляется не в главном потоке, но чтобы использовать эти данные для изменения UI мы перейдем в главный поток.
Так как в силу реализации кода, замыкание которое мы передаем в метод загрузки данных с url, переживет сам метод, то для Swift 3 необходимо явно обозначить его @escaping, а self сделать unowned, чтобы не происходило захвата и удержания ссылки self в этом замыкании. Но это уже нюансы реализации самого языка Swift, а не техонологии получения данных по API.
В некоторых случаях происходят редиректы. Например, если у нас имеется некоторый короткий url, то когда мы вводим его в поисковую строку браузера, браузер сначала идет на сервер, где этот короткий url расшифровывается и отправляется к нам, а затем уже по этому полному url мы переходим на целевой сервер. При необходимости мы можем контролировать эти редиректы с помощью NSURLSessionTaskDelegate, но по умолчанию NSURLSession сама справляется со всеми деталями.
Сериализация — это процесс перевода данных из одного вида хранения в другой, без потери содержания. Например, хранятся данные в двоичном виде, чтобы занимать меньше места, а при пересылке по сети их преобразуют в универсальный JSON (JavaScript Object Notation) формат, который уже мы расшифровываем и переводим в объекты нашей среды программирования.
Пример JSON:
Фигурные скобки обозначают словарь (dictionary), а объекты внутри словаря представлены парами ключ-значение.
В нашем случае API представлен адресом, откуда мы будет получать случайные шутки и форматов JSON ответа, который нам нужно разобрать в удобные для манипулирования структуры
Пример icndb API:
Весь проект, как и прошлый раз, реализован в коде, без использования 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 вторую часть приложения, которая позволяет получать новые шутки, не перезапуская программу, а также голосовать за шутки.
Краткая теория
Формат url
http://www.google.com/?q=Hello&safe=off
- http — протокол, который определяет, по какому стандарту делается запрос. Еще варианты: https, ftp, file
www.google.com— имя домена- / — директория, где находятся необходимые нам ресурсы.
- После вопросительного знака (?) идут параметры q=Hello&safe=off. Они состоят из пар ключ-значение.
- При запросе также указывается метод, который говорит, как сервер должен обрабатывать этот запрос. По умолчанию, это метод GET.
Данный url из примера можно прочитать таким образом: http запрос с методом GET отправляется домену google.com, в корневую директорию /, с двумя параметрами q со значением Hello и safe со значением off.
http заголовок
Браузер преобразует строку url в заголовок и тело запроса. Для http-запроса тело пустое, а заголовок представлен следующим образом
GET /?q=Hello&safe=off HTTP/1.1 Host: google.com Content-Length: 133 // здесь пустая строка // и здесь пустая строка
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) формат, который уже мы расшифровываем и переводим в объекты нашей среды программирования.
Пример JSON:
{ "name": "Martin Conte Mac Donell", "age": 29, "username": "fz" }
Фигурные скобки обозначают словарь (dictionary), а объекты внутри словаря представлены парами ключ-значение.
API (Application Programming Interface)
В нашем случае API представлен адресом, откуда мы будет получать случайные шутки и форматов JSON ответа, который нам нужно разобрать в удобные для манипулирования структуры
http://api.icndb.com/jokes/random
Пример icndb API:
{ "type": "success", "value": { "id": 201, "joke": "Chuck Norris was what Willis was talkin’ about" } }
А теперь практика
Весь проект, как и прошлый раз, реализован в коде, без использования 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, то и добавляем следующий код:
<key>NSAppTransportSecurity</key> <dict> <key>NSAllowsArbitraryLoads</key> <false/> <key>NSExceptionDomains</key> <dict> <key>api.icndb.com</key> <dict> <key>NSExceptionAllowsInsecureHTTPLoads</key> <true/> <key>NSIncludesSubdomains</key> <true/> </dict> </dict> </dict>
Мы, как и по умолчанию, запрещает произвольные загрузки: ключ NSAllowsArbitraryLoads в false. Но добавляем в виде исключения наш домен с шутками и все поддомены: значения ключа NSExceptionDomains.
Теперь в AppDelegate.swift переписываем application(_:didFinishLaunchingWithOptions:) следующим образом:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { self.window = UIWindow(frame: UIScreen.main.bounds) // Объект MainViewController встраиваем в NavigationController, // который понадобиться во второй части. let navC: UINavigationController = UINavigationController(rootViewController: MainViewController()) self.window?.rootViewController = navC self.window?.backgroundColor = UIColor.white self.window?.makeKeyAndVisible() return true }
Создаем файл HTTPCommunication.swift. И пишем в нем следующий код.
import UIKit // Наследуем от NSObject, чтобы подчиняться (conform) NSObjectProtocol, // потому что URLSessionDownloadDelegate наследует от этого протокола, // а раз мы ему подчиняемся, то должны и родительскому протоколу. class HTTPCommunication: NSObject { // Свойство completionHandler в классе - это замыкание, которое будет // содержать код обработки полученных с сайта данных и вывода их // в интерфейсе нашего приложения. var completionHandler: ((Data) -> Void)! // retrieveURL(_: completionHandler:) осуществляет загрузку данных // с url во временное хранилище func retrieveURL(_ url: URL, completionHandler: @escaping ((Data) -> Void)) { } } // Мы создаем расширение класса, которое наследует от NSObject // и подчиняется(conforms) протоколу URLSessionDownloadDelegate, // чтобы использовать возможности данного протокола для обработки // загруженных данных. extension HTTPCommunication: URLSessionDownloadDelegate { // Данный метод вызывается после успешной загрузки данных // с сайта во временное хранилище для их последующей обработки. func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) { } }
Теперь распишем код данных функций.
Копируем код retrieveURL(_ url:, completionHandler:)
// С замыканием мы будем работать вне этой функции, // поэтому мы обозначаем ее @escaping. func retrieveURL(_ url: URL, completionHandler: @escaping ((Data) -> Void)) { self.completionHandler = completionHandler let request: URLRequest = URLRequest(url: url) let session: URLSession = URLSession(configuration: .default, delegate: self, delegateQueue: nil) let task: URLSessionDownloadTask = session.downloadTask(with: request) // Так как задача всегда создается в остановленном состоянии, // мы запускаем ее. task.resume() }
Копируем код func urlSession(_ session:, downloadTask:, didFinishDownloadingTo:)
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) { do { // Мы получаем данные на основе сохраненных во временное // хранилище данных. Поскольку данная операция может вызвать // исключение, мы используем try, а саму операцию заключаем // в блок do {} catch {} let data: Data = try Data(contentsOf: location) // Далее мы выполняем completionHandler с полученными данными. // А так как загрузка происходила асинхронно в фоновой очереди, // то для возможности изменения интерфейса, которой работает в // главной очереди, нам нужно выполнить замыкание в главной очереди. DispatchQueue.main.async(execute: { self.completionHandler(data) }) } catch { print("Can't get data from location.") } }
Создаем файл MainViewController.swift и копируем следующий код, который создает необходимый интерфейс:
import UIKit class MainViewController: UIViewController { lazy var jokeLabel: UILabel = { let label: UILabel = UILabel(frame: CGRect.zero) label.lineBreakMode = .byWordWrapping label.textAlignment = .center label.numberOfLines = 0 label.font = UIFont.systemFont(ofSize: 16) label.sizeToFit() self.view.addSubview(label) return label }() // Идентификатор шутки понадобится для второй части статьи. var jokeID: Int = 0 // ActivityView индикатор будет вращаться, пока не будет // получена шутка, затем он исчезнет. lazy var activityView: UIActivityIndicatorView = { let activityView: UIActivityIndicatorView = UIActivityIndicatorView(activityIndicatorStyle: .gray) activityView.hidesWhenStopped = true activityView.startAnimating() view.addSubview(activityView) return activityView }() lazy var stackView: UIStackView = { let mainStackView: UIStackView = UIStackView(arrangedSubviews: [self.jokeLabel]) // Расстояние между элементами понадобиться во второй части mainStackView.spacing = 50 mainStackView.axis = .vertical mainStackView.distribution = .fillEqually self.view.addSubview(mainStackView) return mainStackView }() override func viewDidLoad() { super.viewDidLoad() self.title = "Chuck Norris Jokes" // В данном методе настраивается stackView и activityView, // что вызывает инициализацию их ленивых переменных. // В свою очередь инициализация stackView вызывает // инициализацию ленивой переменной label. self.configConstraints() // (E.2) // Данный метод содержит весь функционал по работе // с интернетом и получению шутки. self.retrieveRandomJokes() // (E.3) } func retrieveRandomJokes() { } } extension MainViewController { func configConstraints() { // Задаем перевод autoresizingMask в ограничения(constraints) // как false, чтобы не создавать конфликт с нашими собственными // ограничениями self.stackView.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ self.stackView.centerYAnchor.constraint(equalTo: self.view.centerYAnchor), self.stackView.leadingAnchor.constraint(equalTo: self.view.layoutMarginsGuide.leadingAnchor), self.stackView.trailingAnchor.constraint(equalTo: self.view.layoutMarginsGuide.trailingAnchor) ]) self.activityView.translatesAutoresizingMaskIntoConstraints = false // Активируем массив ограничений (constraints) для activityView, // чтобы он показывался на месте label: центр по X и Y равен // центру label по X и Y. NSLayoutConstraint.activate([ self.activityView.centerXAnchor.constraint(equalTo: self.jokeLabel.centerXAnchor), self.activityView.centerYAnchor.constraint(equalTo: self.jokeLabel.centerYAnchor) ]) } }
Разобрались с интерфейсом, теперь можно заполнять функционал.
Вот код retrieveRandomJokes()
func retrieveRandomJokes() { let http: HTTPCommunication = HTTPCommunication() // Посколько мы жестко кодируем url в код, то и сразу force unwrap его // Если url невалидный, то наше приложение уже бесполезно let url: URL = URL(string: "http://api.icndb.com/jokes/random")! http.retrieveURL(url) { // Чтобы избежать захвата self в замыкании, делаем weak self [weak self] (data) -> Void in // Получаем и распечатываем строковое представление json // данных, чтобы знать, в какой формат их переводить. Если // не можем получить нормальный json из загруженных данных, // то дальше уже не идем. guard let json = String(data: data, encoding: String.Encoding.utf8) else { return } // Пример распечатки: JSON: { "type": "success", "value": // { "id": 391, "joke": "TNT was originally developed by Chuck // Norris to cure indigestion.", "categories": [] } } print("JSON: ", json) do { let jsonObjectAny: Any = try JSONSerialization.jsonObject(with: data, options: []) // Проверяем, что мы можем переводить данные из Any // в нужный нам формат, иначе дальше не идем. guard let jsonObject = jsonObjectAny as? [String: Any], let value = jsonObject["value"] as? [String: Any], let id = value["id"] as? Int, let joke = value["joke"] as? String else { return } // Когда данные получены и расшифрованы, // мы останавливаем наш индикатор и он исчезает. self.activityView.stopAnimating() self.jokeID = id self.jokeLabel.text = joke } catch { print("Can't serialize data.") } } }
Теперь запускаем приложение и получаем следующий результат.
Пока мы ждем получения шутки с сайта.

Наконец, шутка загружена и отображена.

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