Эта статья является продолжение статьи Взаимодействие с сервером через API в iOS на Swift 3. Часть 1 и обновлением старой статьи, написанной на Objective-C, на swift 3.

Краткая теория


GET запрос


GET – это простейший метод HTTP запроса, и именно его использует браузер для загрузки веб-страниц. Он используется для запроса содержимого, расположенного по определенному URL. Содержимое может быть, например, веб-страницей, рисунком или аудио-файлом. По соглашению, GET запросы осуществляют только чтение и в соответствии с W3C стандартом не должны быть использованы в операциях, изменяющий серверную сторону. Например, мы не будем использовать GET запрос для отсылания формы или пересылки фотографии, потому что эти операции требуют некоторых изменений на серверной стороне (мы будем использовать в этих случаях POST).

POST запрос


POST посылает данные для дальнейшей обработки на URL. Параметры включены в тело запроса, использующего тот же формат, что и GET. Например, если мы хотим запостить форму, содержающую два поля, имя и возраст, то мы пошлем что-то похожее на name=Martin&age=29 в теле запроса. 

Такой способ пересылки параметров широко используется в веб-страницах. Наиболее популярные случаи – это формы. Когда мы заполняем форму на сайте и кликаем Submit, вероятнее всего запрос будет POST.

В нашем приложении мы используем POST для выставления рейтинга шуток. Мы будем посылать голоса (или +1, или -1) на удаленный сервер. 

Данные POST запроса могут быть структурированы с использованием разных форматов. Параметры обычно отформатированы в соответствии со стандартами form-url-кодирования (в соответствии с W3C HTML стандартом). Это формат по умолчанию и широко используется во многих браузерах. Наш метод принимает словарь Dictionary в качестве аргумента, но мы не может послать по HTTP соединению словарь Dictionary, потому что это внутренний тип Swift. Для пересылки по HTTP-соединению нам надо создать распознаваемое представление словаря. Это как общение с иностранцем. Мы переводим наше осообщение на универсальный язык, а он переводит с универсального языка уже на свой родной. Универсальнйй язык в HTTP — это W3C стандарт, наш язык — это Swift, язык получателя нам неизвестен.

W3C стандарт задает правила, определяющие, что означает распознаваемое представление для каждого случая. В нашем случае нам нужно представить параметры, следующие form-url-закодированной части стандарта (например, param1=var1&param2=var2).

WebView


С помощью средств языка программирования мы можем посылать запросы на удаленный сервер. Именно это делают браузеры перед отображением веб-страницы. Отличие только в содержимом ответа. Веб-страницы форматированны с помощью HTML стандарта, который определяет ряд правил на то, как графически определить различные теги разметки. Эти правила кажутся простыми, но отображение целой страницы, следующей W3C стандарту – это сложная задача. К счастью, в iOS есть встроенный компонент UIWebView, который использует хорошо известный движок WebKit, и интерпретирует HTML/CSS/JavaScript, и отображает целые веб-страницы внутри UIView.

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

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

webView:shouldStartLoadWithRequest:navigationType:
webViewDidStartLoad:
webViewDidFinishLoad:
webView:didFailLoadWithError:

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

Реализация голосования через POST запрос


Мы будем дописывать файлы MainViewController.swift и HTTPCommunication.swift, и создадим новый файл WebViewController.swift, в котором определим нашу webView.

Сначала создаем новый файл WebViewController.swift и в нем определяем класс WebViewController:

WebViewController.swift

import UIKit

class WebViewController: UIViewController {
}

extension WebViewController {
}

Теперь мы реализуем функционал для совершения POST запросов в классе, ответственном за все наши HTTP-операции: HTTPCommunication класс. Для этого в HTTPCommunication.swift добавим новый метод postURL(_:params:completionHandler:), который похож на предыдущий retrieveURL(_:completionHandler:) метод.

HTTPCommunication.swift

class HTTPCommunication: NSObject {
    …
    func retrieveURL(_ url: URL, completionHandler: @escaping ((Data) -> Void)) { … }
    // TODO: СЮДА ВСТАВЛЯЕМ КОД
} // end of class HTTPCommunication

Код метода

func postURL(_ url: URL, params: [String: Int], completionHandler: @escaping ((Data) -> Void)) { // (K.1)
    self.completionHandler = completionHandler // (K.2)
    
    func postBody(params: [String: Int]) -> Data? { // (K.3)
        var paramsArr: [String] = []
        for (key, value) in params {
            paramsArr.append("\(key)=\(value)")
        }
        let postBodyString: String = paramsArr.joined(separator: "&")
        let postBodyData: Data? = postBodyString.data(using: .utf8)
        return postBodyData
    }

    var request: URLRequest = URLRequest(url: url) // (K.4)
    request.httpMethod = "POST"
    request.httpBody = postBody(params: params)

    let session: URLSession = URLSession(configuration: .default, delegate: self, delegateQueue: nil) // (K.5)		
    let task: URLSessionDownloadTask = session.downloadTask(with: request)	
    task.resume()
}

(K.1) С замыканием мы будем работать вне этой функции, поэтому мы обозначаем ее @escaping.
(K.2) Мы сохраняем переданное замыкание в свойство completionHandler.
(K.3) Пишем метод, который преобразуем словарь переданных параметров в строковую форму, а потом кодируем эту строку в тип Data в utf8 кодировке.
(K.4) Создаем запрос с переданным url и настраиваем его: в качестве метода выставляем POST, а телом запроса делаем наши параметры типа Data с utf8.
(K.5) Наконец, создаем сессию с конфигурацией по умолчанию и делегатом в качестве самих себя. И создаем задачу (task) для загрузки результата данного запроса во временное хранилище. В конце, размораживаем задачу.

Теперь допишем в MainViewController.swift необходимый интерфейс. Добавим две кнопки для голосования voteUp и voteDown, которые соответственно будут повышать или понижать рейтинг текущей шутки. Также добавим кнопку “Chuck Who?”, которую мы наполним функционалом в разделе про web view.

В MainViewController.swift вставляем код инициализации voteUpButton, voteDownButton и chuckWhoButton.

MainViewController.swift

lazy var jokeLabel: UILabel! = …
var jokeID: Int!
lazy var activityView: UIActivityIndicatorView! = …

//TODO: СЮДА ВСТАВЛЯЕМ КОД

lazy var stackView: UIStackView! = …

Код инициализации кнопок
lazy var voteUpButton: UIButton! = { // (L.1)
    let button: UIButton = UIButton(type: .system) // (L.2)
    button.setTitle("Vote Up", for: .normal)
    button.sizeToFit()
    button.addTarget(self, action: #selector(self.voteUp), for: .touchUpInside) // (L.3)
    self.view.addSubview(button) // (L.4)
    return button // (L.5)
}()

lazy var voteDownButton: UIButton! = {
    let button: UIButton = UIButton(type: .system)
    button.setTitle("Vote Down", for: .normal)
    button.sizeToFit()
    button.addTarget(self, action: #selector(self.voteDown), for: .touchUpInside)
    self.view.addSubview(button)
    return button
}()

lazy var chuckWhoButton: UIButton! = {
    let button: UIButton = UIButton(type: .system)
    button.setTitle("Chuck Who?", for: .normal)
    button.sizeToFit()
    button.addTarget(self, action: #selector(self.chuckWho), for: .touchUpInside)
    self.view.addSubview(button)
    return button
}()

Код инициализации всех трех кнопок однотипен.

(L.1) Мы объявляем ленивую переменную и инициализируем ее замыканием.
(L.2) Внутри замыкания мы определяем локальную переменную для кнопку, системного типа, задаем название и размер.
(L.3) Ассоциируем с кнопкой соответствующее действие при нажатии.
(L.4) Добавляем в иерархию вьюх.
(L.5) И присваиваем эту локальную переменную нашей ленивой переменной.

Далее в MainViewController.swift вставляем код методов для кнопок

MainViewController.swift

class MainViewController: UIViewController {
    …
    func retrieveRandomJokes() { … }
    // TODO: СЮДА ВСТАВЛЯЕМ КОД
} // end of class MainViewController

Код методов для кнопок
func voteUp() {
    let http: HTTPCommunication = HTTPCommunication() // (M.1)
    let params: [String: Int] = ["joke_id": self.jokeID, "vote": 1] // (M.2)
    if let url = URL(string: "http://example.com/rater/vote") { // (M.3)
        http.postURL(url, params: params) { (data) in // (M.4)
            print("\(data). Voted Up")
        }
    }
}

func voteDown() {
    let http: HTTPCommunication = HTTPCommunication()
    let params: [String: Int] = ["joke_id": self.jokeID, "vote": -1]
    if let url = URL(string: "http://example.com/rater/vote") {
        http.postURL(url, params: params) { (data) in
            print("\(data). Voted Down")
        }
    }
}

func chuckWho() {
}

Код методов для голосования up и down однотипен.

(M.1) Создаем объект класса для связи с сервером.
(M.2) Создаем словарь параметров, который включает id шутки, за которую голосуем, и собственно значение голоса +1 или -1.
(M.3) Далее безопасно получаем url из строки адреса нашего тренировочного домена http://example.com/rater/vote.
(M.4) И посылаем через POST запрос на url наши параметры. И распечатываем в консоль строку, свидетельствующую о выполнении запроса.

Далее в MainViewController.swift переписываем код метода configStackView с учетом внесенных в UI изменений.

MainViewController.swift

func configStackView() -> UIStackView {
    let innerStackView: UIStackView = UIStackView(arrangedSubviews: [self.voteUpButton, self.voteDownButton]) // (N.1)
    innerStackView.axis = .horizontal // (N.2)
    innerStackView.distribution = .fillEqually

    let mainStackView: UIStackView = UIStackView(arrangedSubviews: [self.jokeLabel, innerStackView, self.chuckWhoButton]) // (N.3)
    mainStackView.spacing = 50 // (N.4)
    mainStackView.axis = .vertical
    mainStackView.distribution = .fillEqually

    self.view.addSubview(mainStackView) // (N.5)
    return mainStackView
}

(N.1) Создаем innerStackView, содержающую две кнопки для голосования.
(N.2) Выставляем оси вертикальные; распределение одинаковое.
(N.3) В главный mainStackView к лэйбл из прошлой части добавляем innerStackView и кнопку про Чака Норриса.
(N.4) Настраиваем mainStackView.
(N.5) Добавляем mainStackView в иерархию вьюх.

Запускаем наше приложение и видим интерфейс как на Рис. 1


Рис.1 Интерфейс нашего приложения с добавленными кнопками для голосования

проверяем, что при голосовании за шутку или против нее в консоли выдаются сообщения: 

345 bytes. Voted Up

или 

345 bytes. Voted Down

что свидетельствует об успешно выполненном POST-запросе на сервер.

Мы написали приложение для получения шуток с использованием icndb API и GET HTTP глагола. Мы смогли показать эти шутки на UIView и каждую шутку можно оценить. Эти действия посылают POST запрос на удаленный сервер, который должен сохранить нашу оценку. 

Использование webViews для отображения веб-страниц


Мы будем добавлять webView для отображения страницы на википедии о Чаке Норрисе. Она запутится по нажатию кнопки. 

В файле WebViewController.swift пишем следующий стаб функций:

WebViewController.swift

import UIKit

class WebViewController: UIViewController {

    lazy var webView: UIWebView = { // (O.1)
        self.configWebView()
    }()

    lazy var toolbar: UIToolbar = { // (O.2)
        self.configToolbar()
    }()

    override func viewDidLoad() {
        super.viewDidLoad()

    }

    func back() { // (O.3)
        self.webView.goBack()
    }

    func forward() {
        self.webView.goForward()
    }
}

extension WebViewController {
    func configWebView() -> UIWebView {  // (O.4)
      
    }

    func configToolbar() -> UIToolbar {  // (O.5)
    }

    func configConstraints() {  // (O.6)
    }
}

(O.1) Мы создаем ленивые переменные для webView
(O.2) и toolbar.
(O.3) Создаем методы back и forward, для для навигации по истории посещения ссылок.
(O.4) В расширение выносим методы инициализации webView
(O.5) и toolBar
(O.6) и ограничений (constraints) для интерфейса.

Теперь расписываем код каждой функции:

func configWebView() -> UIWebView {
    let webView: UIWebView = UIWebView() // (P.1)
    self.view.addSubview(webView)
    return webView
}

(P.1) Инициализируем webView и добавляем в иерархию вьюх.

func configToolbar() -> UIToolbar {
    let toolbar: UIToolbar = UIToolbar(frame: CGRect.zero) // (Q.1)
    let backButton: UIBarButtonItem = UIBarButtonItem(title: "<", style: .plain, target: self, action: #selector(self.back)) // (Q.2)
    let forwardButton: UIBarButtonItem = UIBarButtonItem(title: ">", style: .plain, target: self, action: #selector(self.forward))
    toolbar.setItems([backButton, forwardButton], animated: false) // (Q.3)
    toolbar.backgroundColor = UIColor.lightGray // (Q.4)
    self.view.addSubview(toolbar)
    return toolbar
}

(Q.1) Создаем toolbar с нулевым фреймом.
(Q.2) Создаем две кнопки для навигации по истории посещения ссылок. С названиями “<“ и “>” и экшенами, привязанными к соответствующим методом в webView.
(Q.3) Добавляем в toolbar эти кнопки.
(Q.4) Ставил цвет фона, как и у navigationBar, светло-серый. И добавляем в иерархию вьюх.

func configConstraints() {
    self.webView.translatesAutoresizingMaskIntoConstraints = false // (R.1)
    NSLayoutConstraint.activate([ // (R.2)
        self.webView.topAnchor.constraint(equalTo: self.topLayoutGuide.topAnchor),
        self.webView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
        self.webView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
        self.webView.bottomAnchor.constraint(equalTo: self.toolbar.topAnchor)
    ])

    self.toolbar.translatesAutoresizingMaskIntoConstraints = false
    NSLayoutConstraint.activate([ // (R.3)
        self.toolbar.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
        self.toolbar.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
        self.toolbar.bottomAnchor.constraint(equalTo: self.view.bottomAnchor),
        self.toolbar.heightAnchor.constraint(equalToConstant: 44.0)
    ])
}

(R.1) Отключаем autoresizing masks, чтобы они не конфликтовали с нашими ограничениями (constraints)
(R.2) Для webView настраиваем все четыре стороны вьюхи.
(R.3) Для toolbar настраиваем три стороны вьюхи, кроме верхней, но дополнительно выставляем высоту в стандартный размер 44.0.

И переписываем код ViewDidLoad() следующим образом:

override func viewDidLoad() {
    super.viewDidLoad()

    self.configConstraints() // (S.1)

    if let url = URL(string: "http://en.wikipedia.org/wiki/Chuck_Norris") {  // (S.2)
        let request: URLRequest = URLRequest(url: url) // (S.3)
        self.webView.loadRequest(request) // (S.4)
    }
}

(S.1) Вызываем настройку ограничений (constraints).
(S.2) Безопасно получаем url из строки адреса страницы Википедии en.wikipedia.org/wiki/Chuck_Norris
(S.3) Создаем запрос из url.
(S.4) И выполняем данный запрос в webView.

Возвращаемся в MainViewController и заполняем методом вызова информации о Чаке Норрисе.

MainViewController.swift

func chuckWho() {
    self.navigationController?.show(WebViewController(), sender: self) // (T.1)
}

(T.1) При нажатии на данную кнопку на стек navigationController кладем новый экземпляр webView.

Рис. 2 демонстрирует, как webView выглядит в нашем приложении.


Рис. 2. Финальный вид реализованного нами webView

Комментарии (1)


  1. khlopko
    29.06.2018 03:40

    Вы конечно извините, но это ужасно. Ладно что 3-я версия языка, не так много поменялось, но:
    — полный хаос в написании — в каждом файле/классе по-разному;
    — говорил в комментариях к прошлой статье что force optional unwrapping не доглядели, но тут используете его вовсю, при чем и где не надо тоже;
    — lazy var (да еще и в паре с предыдущим пунктом) это просто что-то с чем-то;
    — почему все еще не DataTask для простейших запросов не понятно;

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