Краткая теория
Формат 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 добавляем строку App Transport Security Settings и для этих настроек создаем ключ Allow Arbitrary Loads, которые выставляем в YES. Если открыть Info.plist как source code, то наш добавляемый код выглядит так:
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
Теперь в AppDelegate.swift переписываем application(_:didFinishLaunchingWithOptions:) следующим образом:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
self.window = UIWindow(frame: UIScreen.main.bounds) // (A.1)
let navC: UINavigationController = UINavigationController(rootViewController: MainViewController()) // (A.2)
self.window?.rootViewController = navC
self.window?.backgroundColor = UIColor.white // (A.3)
self.window?.makeKeyAndVisible() // (A.4)
return true
}
(A.1) Мы задаем размер окна равный размеру экрана UScreen.main.bound.
(A.2) Создаем MainViewController и сразу embed его в NavigationController, который понадобиться во второй части. NavigationController делает rootViewController окна.
(A.3) Цвет фона белый
(A.4) И не забывает сделать окно ключевым и видимым.
Создаем файл HTTPCommunication.swift. И пишем в нем следующий код.
import Foundation
class HTTPCommunication: NSObject { // (B.1)
var completionHandler: ((Data) -> Void)! // (B.2)
func retrieveURL(_ url: URL, completionHandler: @escaping ((Data) -> Void)) { // (B.3)
}
}
extension HTTPCommunication: URLSessionDownloadDelegate { // (B.4)
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) { // (B.5)
}
}
(B.1) Наследуем от NSObject, чтобы conforms NSObjectProtocol, потому что URLSessionDownloadDelegate наследует от этого протокола, а раз мы ему подчиняемся(conforms), то должны и родительскому протоколу.
(B.2) Свойство completionHandler в классе — это замыкание, которое будет содержать код обработки полученных с сайта данных и вывода их в интерфейсе нашего приложения.
(B.3) retrieveURL(_: completionHandler:) осуществляет загрузку данных с url во временное хранилище
(B.4) Мы создаем расширение класса, которое наследует от NSObject и подчиняется(conforms) протоколу URLSessionDownloadDelegate, чтобы использовать возможности данного протокола для обработки загруженных данных.
(B.5) urlSession(_:downloadTask: didFinishDownloadingTo) вызывается после успешной загрузки данных с сайта во временное хранилище для их последующей обработки.
Теперь распишем код данных функций.
Копируем код retrieveURL(_ url:, completionHandler:)
func retrieveURL(_ url: URL, completionHandler: @escaping ((Data) -> Void)) { // (C.1)
self.completionHandler = completionHandler // (C.2)
let request: URLRequest = URLRequest(url: url) // (C.3)
let conf: URLSessionConfiguration = URLSessionConfiguration.default // (C.4)
let session: URLSession = URLSession(configuration: conf, delegate: self, delegateQueue: nil)
// (C.5)
let task: URLSessionDownloadTask = session.downloadTask(with: request) // (C.6)
task.resume() // (C.7)
}
(C.1) С замыканием мы будем работать вне этой функции, поэтому мы обозначаем ее @escaping.
(C.2) Мы сохраняем переданное замыкание в свойство completionHandler.
(C.3) Инициализируем запрос переданным url.
(C.4) Создаем конфигурацию сессии по умолчанию.
(C.5) Создаем сессию.
(C.6) Создаем задачу загрузки.
(C.7) Так как задача всегда создается в остановленном состоянии, мы запускаем ее.
Копируем код func urlSession(_ session:, downloadTask:, didFinishDownloadingTo:)
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
do {
let data: Data = try Data(contentsOf: location) // (D.1)
DispatchQueue.main.async(execute: { // (D.2)
self.completionHandler(data)
})
} catch {
print("Can't get data from location.")
}
}
(D.1) Мы получаем данные на основе сохраненных во временное хранилище данных. Поскольку данная операция может вызвать исключение, мы используем try, а саму операцию заключаем в блок do {} catch {}
(D.2) Далее мы выполняем completionHandler с полученными данными. А так как загрузка происходила асинхронно в фоновой очереди, то для возможности изменения интерфейса, которой работает в главной очереди, нам нужно выполнить замыкание в главной очереди.
Создаем файл MainViewController.swift и копируем следующий код
import UIKit
class MainViewController: UIViewController {
lazy var jokeLabel: UILabel = { // (E.1)
self.configLabel()
}()
var jokeID: Int = 0
lazy var activityView: UIActivityIndicatorView = {
self.configActivityView()
}()
lazy var stackView: UIStackView! = {
self.configStackView()
}()
override func viewDidLoad() {
super.viewDidLoad()
self.title = "Chuck Norris Jokes"
self.configConstraints() // (E.2)
self.retrieveRandomJokes() // (E.3)
}
func retrieveRandomJokes() {
}
}
extension MainViewController { // (E.4)
func configActivityView() -> UIActivityIndicatorView {
}
func configLabel() -> UILabel {
}
func configStackView() -> UIStackView {
}
func configConstraints() {
}
}
(E.1) Создаем label, которая будет отображать шутку про Чака Норриса. Идентификатор (id) шутки понадобится для второй части статьи. ActivityView индикатор будет вращаться, пока не будет получена шутка, затем он исчезнет. StackView используется для визуального представления (layout).
(E.2) В viewDidLoad() вызываем configConstraints(), в которой настраивается stackView и activityView, что вызывает инициализацию их ленивых переменных. В свою очередь инициализация stackView вызывает инициализацию ленивой переменной label.
(E.3) Также в viewDidLoad() вызываем retrieveRandomJokes(), которая содержит весь функционал по работе с интернетом и получению шутки.
(E.4) Настройка label, activityView, stackView и constraints вынесены в расширение.
Теперь подробнее о каждой функции.
Копируем код configActivityView()
func configActivityView() -> UIActivityIndicatorView {
let activityView: UIActivityIndicatorView = UIActivityIndicatorView(activityIndicatorStyle: .gray)
// (F.1)
activityView.hidesWhenStopped = true // (F.2)
activityView.startAnimating() // (F.3)
view.addSubview(activityView) // (F.4)
return activityView
}
(F.1) Создаем actitivyView серого цвета.
(F.2) Когда он перестанет вращаться, он исчезнет.
(F.3) Начинаем анимацию вращения.
(F.4) И добавляем в иерархию вьюх.
Копируем код configLabel()
func configLabel() -> UILabel {
let label: UILabel = UILabel(frame: CGRect.zero) // (G.1)
label.lineBreakMode = .byWordWrapping
label.textAlignment = .center
label.numberOfLines = 0
label.font = UIFont.systemFont(ofSize: 16)
label.sizeToFit() // (G.2)
self.view.addSubview(label) // (G.3)
return label
}
(G.1) Создаем label с первоначальным нулевым фреймом; задаем переносы строк по словам; задаем выравнивание по центру; делаем label многострочной, для чего выставляем количество строк в 0; выставляем системный шрифт размера 16.
(G.2) Для того, чтобы label по размеру соответствовал внутреннему содержимому, вызываем sizeToFit().
(G.3) И добавляем в иерархию вьюх.
Копируем код configStackView()
func configStackView() -> UIStackView {
let mainStackView: UIStackView = UIStackView(arrangedSubviews: [self.jokeLabel]) // (H.1)
mainStackView.spacing = 50 // (H.2)
mainStackView.axis = .vertical
mainStackView.distribution = .fillEqually
self.view.addSubview(mainStackView) // (H.3)
return mainStackView
}
(H.1) Создаем stackView, содержающую label.
(H.2) Настраиваем расстояние между элементами 50, оно понадобиться во второй части; оси — вертикальные; распределение одинаковое.
(H.3) И добавляем в иерархию вьюх.
Копируем код configConstraints()
func configConstraints() {
self.stackView.translatesAutoresizingMaskIntoConstraints = false // (I.1)
NSLayoutConstraint.activate([ // (I.2)
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 // (I.1)
NSLayoutConstraint.activate([ // (I.3)
self.activityView.centerXAnchor.constraint(equalTo: self.jokeLabel.centerXAnchor),
self.activityView.centerYAnchor.constraint(equalTo: self.jokeLabel.centerYAnchor)
])
}
(I.1) Задаем перевод autoresizingMask в ограничения(constraints) как false, чтобы не создавать конфликт с нашими собственными ограничениями(constraits).
(I.2) Активируем сразу массив ограничений (constraints) в количестве трех: центр по оси Y равен центру view по оси Y; leading равен leading margins и trailing равен trailing margins.
(I.3) Активируем массив ограничений (constraints) для activityView, чтобы он показывался на месте label: центр по X и Y равен центру label по X и Y.
Разобрались с интерфейсом, теперь можно заполнять функционал.
Вот код retrieveRandomJokes()
func retrieveRandomJokes() {
let http: HTTPCommunication = HTTPCommunication() // (J.1)
guard let url = URL(string: "http://api.icndb.com/jokes/random") else { return } // (J.2)
http.retrieveURL(url) { // (J.3)
[unowned self] (data) -> Void in // (J.4)
let json: String = String(data: data, encoding: String.Encoding.utf8)! // (J.5)
print("JSON: ", json)
do {
let jsonObject: [String: Any] = try JSONSerialization.jsonObject(with: data, options: []) as! [String: Any] // (J.6)
if let value = jsonObject["value"] as? [String: Any], let id = value["id"] as? Int, let joke = value["joke"] as? String { // (J.7)
self.activityView.stopAnimating() // (J.8)
self.jokeID = id // (J.9)
self.jokeLabel.text = joke
}
} catch {
print("Can't serialize data.")
}
}
}
(J.1) Создаем http — экземпляр класса HTTPCommunication, который выполняет все запросы.
(J.2) Через guard из строки адреса получаем url, с которым будем дальше работать, или возвращаемся из метода в противном случае.
(J.3) Мы вызываем метод retrieveURL() для объекта http и передаем ему в качестве параметров наш url и наше замыкание типа (data: Data) -> Void. Это замыкание и содержит код по обработке уже полученных “сырых” данных и извлечения из них шутки. Поскольку замыкание стоит последним параметром, то применяем синтаксический сахар и передаем замыкание сразу за скобками.
(J.4) Посколько замыкание @escaping, то есть “переживет” вызов функции, то оно будет удерживать self. Чтобы это избежать, ставим unowned self.
(J.5) Сначала мы переводим сырые данные в строковый формат с кодировкой utf8 и распечатываем их. Делаем это только для того, чтобы узнать формат пересланных данных. Формат следующий:
JSON: { "type": "success", "value": { "id": 391, "joke": "TNT was originally developed by Chuck Norris to cure indigestion.", "categories": [] } }
Как видим, это словарь с двумя ключами: type и “value”, нас интересует ключ “value”, который также является словарем с ключами “id”, “joke” и “categories”.
(J.6) Затем сериализуем JSON данные в словарь типа [String: Any]. Поскольку сериализация может вызвать исключение мы вызываем ее с try, а сам код заключаем в блок do {} catch {}
(J.7) Выполняем три последовательных опциональных связывания (optional binding) и получаем необходимые нам id и joke из словаря.
(J.8) Когда данные наверняка получены и расшифрованы, мы останавливаем наш индикатор и он исчезает.
(J.9) Присваиваем id и joke заданным нами ранее свойствам класса. И label появляется на месте нашего индикатора.
Теперь запускаем приложение и получаем следующий результат.
Пока мы ждем получения шутки с сайта.
Наконец, шутка загружена и отображена.
В следующей статьи мы посмотрим на переписанную на swift вторую часть приложения, которая позволяет получать новые шутки, не перезапуская программу, а также голосовать за шутки.
Комментарии (5)
DjPhoeniX
17.06.2018 23:511. Уже есть Swift 4 с Decodable, JSONSerialization не нужен
2. Использовать URLDownloadTask для API — не лучшая практика, захламлять систему одноразовыми файлами не стоит. DataTask для API был бы более логичен.
3. С форматированием кода (отступы) совсем беда.
4.NSAppTransportSecurity.NSAllowsArbitraryLoads
— PLEASE NO! Используйте TLS для ВСЕГО, в самом крайнем случае разрешайте доступ без шифрования к ограниченному числу доменов…
5. Используйте weak вместо unowned, иначе легко словите Nil Unwrapping.
6. Force Unwrap (xxx!
) — зло в любом виде, особенно (ОСОБЕННО) в декодировании данных из сторонних источников.olegi
18.06.2018 00:22а можно подробнее про 6 пункт? как быть системе если данные не пришли?
DjPhoeniX
18.06.2018 00:53Я не про систему, а про особенность Swift. Swift спроектирован как null-safe язык (привет Java-программистам с их NPE), и любые объекты, которые могут быть «null» объявляются как
Type?
(синтаксический сахар кOptional<Type>
). Обращаться к таким объектам можно определёнными способами, и один из них — «force unwrap»:
let value: Type? = someFunc() print(value) // выведет Optional(value) или nil print(value!) // force unwrap - выведет value
Если в момент «force unwrap» значение переменной (или результата метода) окажется null (nil) — приложение завершит работу с Fatal Error.
Вывод — ВСЕГДА используйте optional unwrap и конструкции вида "if let x = optional { ... }
" / "guard let x = optional else { ... ; return }
"
voidptr0
Интрига в заголовке заставила прочитать статью.
eirnym
"Удалённый ресурс" оказался всего-лишь remote, а не removed или unlinked.
Но статья получилась хорошей