Взаимодействие Swift с JavaScript

Во все приложения для операционной системы iOS встроена поддержка веб-компонентов. Это нужно, преимущественно, для отображения каких-либо веб-страниц. Однако, для взаимодействия с веб-содержимым в приложениях – необходим некий “мост”. От технологии Swift к технологии JavaScript. При разработке своего приложения для iOS я столкнулся с данной проблемой. В моем приложении существует WKWebView, который запускает фреймворк, написанный на JavaScript. Этот фреймворк в свою очередь берет данные для вычислений из базы данных SQLite нативного приложения, производит подсчеты, возвращает новые данные в Swift. Затем эти данные записывает обратно в базу данных. Чуть сложновато. Но речь не об этом.

К счастью компонент WKWebView оснащен таким “мостом” для взаимодействия между Swift и JavaScript. В данной статье разберем (с примером) его работу.

Ключевые компоненты

  • WKWebView – позволяет загрузить веб-контент по URL
  • WKScriptMessage – объект, который создается после выполнения метода postMessage()
  • WKUserContentController – менеджер отправки и получения JavaScript
  • WKScriptMessageHandler – протокол для доступа к методам WKScriptMessage
  • WKWebViewConfiguration – настройки, передаваемые в WKWebView

Краткий обзор

Диаграмма взаимодействия Swift с JavaScript
Диаграмма взаимодействия Swift с JavaScript

Представленная выше диаграмма отражает путь от и до web-view. Термин “doStuff” общий, пожалуйста, подключите ваше воображение для замены его поведением, которое вам нужно 😉

На приведенной выше диаграмме видно, что при правильно написанной программе взаимодействие начинается с компонента WebView. Первым делом запускается программа, написанная на языке Swift. Внутри программы срабатывают сценарии открытия компонента WebView. Данный компонент инициализирует загрузку в отдельном процессе веб-документа html. Затем html документ выполняет вложенный код JavaScript.

Вызов триггера postMessage

После того, как веб-документ загружен, весь код JavaScript обработан – начинается самое интересное. JavaScript вызывает триггер-метод postMessage в обработчике событий doStuffMessageHandler. ( ➡ Позже я покажу как добавить обработчик в коде Swift, который принимает имя doStuffMessageHandler’а. ) Тут важный момент 💡 Название обработчика событий (MessageHandler) может быть абсолютно любым. На ваше усмотрение. Например: turnOnMusic.postMessage(trackName). Важно запомнить, в коде Swift вам нужно будет обработать совпадение только с именем, которое идет до .postMessage(). Оно называется “messageName”, а параметры, которые передаются в скобках postMessage(parameters) – “messageBody”.

Теперь, давайте на примере проверим работоспособность нашей теории. Создадим новый пустой проект Xcode, и добавим в него пустой файл с именем index.html. Откроем этот файл в редакторе Xcode и пропишем следующее:

<html>
    <script>
        window.webkit.messageHandlers.doStuffMessageHandler.postMessage({ param1: "stuff", param2: "1000" });
    </script>
</html>

Чтобы этот код запустился, необходимо сначала загрузить веб-страницу в Swift:

if let url = Bundle.main.url(forResource: "index", withExtension: "html") {
    let request = URLRequest(url: url)
    webView.load(request)
}

В данном примере из JavaScript в Swift передаются два параметра, но вы можете добавлять столько параметров, сколько вам необходимо. JavaScript код можно добавлять несколькими способами. Можно встроить его непосредственно в html страницу, можно указать в html ссылку на .js файл со скриптом. Так же можно инжектировать скрипт с помощью WKUserScripts и метода addUserScript() в userContentController’е. Такого рода скрипты могут выполняться в момент, когда страница загружается или когда она завершила свою загрузку.

“Слушатели” метода postMessage() в iOs приложении

Для того, чтобы JavaScript мог обработать message handler, нужно чтобы была передана его конфигурация в WKWebView во время инициализации программы. Это делается с помощью вызова add() метода из userContentController свойства объекта WKWebViewConfiguration передающего имя handler’а. Класс, принимающий сообщения, также должен быть передан в метод add() как параметр. Рассмотрим пример:

let doStuffMessageHandler = "doStuffMessageHandler"
let configuration = WKWebViewConfiguration()
configuration.userContentController.add(self, name: doStuffMessageHandler)
let rect = CGRect(x:0, y:0, width: frame.width, height: frame.height)
let webView = WKWebView(frame: rect, configuration: configuraion)

Класс, передаваемый в add() метод должен соответствовать протоколу WKScriptMessageHandler. Это свойство разрешит нашему “целевому” классу делегировать методы, которые выполняются в следствии обработки определенного javascript handler’а. В следующем листинге мы расширяем наш класс WebViewController для удовлетворения протокола и добавляем метод didReceiveMessage для обработки событий:

extension WebViewController: WKScriptMessageHandler {
    func userContentController(_ userContentController: WKUserContentController,
                                 didReceive message: WKScriptMessage) {
        if message.name == doStuffMessageHandler {
            guard let dict = message.body as? [String: AnyObject],
                let param1 = dict["param1"] as? String,
                let param2 = dict["param2"] as? Int else {
                    return
            }
        // doStuff(with: param1, param2)
        }
    }
}

Когда срабатывает postMessage триггер, делегирующий метод уведомляет приложение с помощью WKScriptMessage объекта. WKScriptMessage содержит “name”, одинаковое с handler’ом, и “body”, включающее в себя любые параметры или текст. Эти параметры посылаются из JavaScript в Swift. Для обработки сообщения мы должны поймать имя handler’а в Swift, и принять его параметры.

Обратная связь с web-view

Если приложение получило и обработало какие-то данные из JavaScript, почти наверняка оно должно вернуть результат этой обработки. Для обратного сообщения Swift с JavaScript существует метод evaluateJavascript. Этот метод принимает строку типа String, как параметр. Эта строка представляет собой некий код JavaScript или вызов JS функции. В качестве второго параметра идет блок, который срабатывает после отправки сообщения. Он также может вернуть ошибку, возникающую при отправки сообщения.

func doStuffDidFinish(result: DoStuffResult, param1: String, param2: String) {

    let data: [String: String] = [
        "param1": "\(param1)",
        "param2": "\(param2)"
    ]

guard let json = try? JSONEncoder().encode(data),
    let jsonString = String(data: json, encoding: .utf8) else {
        return
}

    switch result {
    case .success:
        let javascript = "window.actions.suffWasSuccessful('\(jsonString)')"
        webView.evaluateJavaScript(javascript, completionHandler: nil)
    case .failure:
        let javascript = "window.actions.stuffFailed('\(jsonString)')"
        webView.evaluateJavaScript(javascript, completionHandler: nil)
    }
}

В коде выше мы передаем данные в формате JSON как строку.

Скрипт который мы “евалуируем”, должен находиться на веб-странице, загруженной в web-view во время его запуска. Если это не так, вы можете посмотреть, как внедрить его с помощью метода addUserScript () в userContentController. Это финальный шаг сообщения приложения с веб-представлением.

В данной статье описана самая суть(ядро) взаимодействия Swift и JavaScript. Тут опущены некоторые подробности, отсутствие которых может сбить с толку новичков в Swift программировании. Дабы избежать замешательства, в ближайшее время я создам учебный проект в Xcode, в котором описанные выше методики можно будет изучить наглядно. И может быть запишу процесс создания этого проекта на видео. Большое спасибо за внимание, надеюсь эти материалы оказались для вас полезны.