いわゆるガワネイティブというやつです。
iOSアプリのView構築に疲れたのでElmでViewを作れればいいんだという思いつきで試してみました。
iOSでHTMLを読み込んで表示する
iosプロジェクト内にHTMLファイルがあればWKWebViewを利用して画面を表示することが出来ます。
以下のようなプロジェクト構造を想定します。
project/
elm-app/
public/
js/
main.js
index.html
iOS側ではHTMLのコードをロードすればその内部にあるスクリプトが実行されるのでElmから生成したjsが動作します。
final class ViewController: UIViewController {
// HTML内のリソース指定がアクセスするためのベースURL
private let baseURL: URL = {
let basePath = Bundle.main.path(forResource: "elm-app/public", ofType: nil)!
return URL(fileURLWithPath: basePath)
}()
// HTMLファイルの文字列を取得
private let contents: String = {
let htmlPath = Bundle.main.path(forResource: "elm-app/public/index", ofType: "html")!
return try! String(contentsOfFile: htmlPath, encoding: .utf8)
}()
self.webView: WKWebView!
override func viewDidLoad() {
super.viewDidLoad()
// WKWebViewをルートViewにセット
self.setWebView()
// WKWebViewにHTMLを読み込む
self.webView.loadHTMLString(self.contents, baseURL: self.baseURL)
}
private func setWebView() {
let webConfiguration = WKWebViewConfiguration()
self.webView = WKWebView(frame: .zero, configuration: webConfiguration)
self.view.addSubview(self.webView)
self.webView.translatesAutoresizingMaskIntoConstraints = false
self.view.addConstraints([
self.webView.topAnchor.constraint(equalTo: self.webView.superview!.topAnchor),
self.webView.leftAnchor.constraint(equalTo: self.webView.superview!.leftAnchor),
self.webView.rightAnchor.constraint(equalTo: self.webView.superview!.rightAnchor),
self.webView.bottomAnchor.constraint(equalTo: self.webView.superview!.bottomAnchor)
])
}
}
JSからiOSへの通信
WKWebViewで生成したブラウザ内部ではwebkit.messageHandlers
というプロパティを通してブラウザ側のイベントを受け取ることが出来ます。
受け取るイベントはiOS側で予め設定する必要があります。
WKWebViewを生成するときに、JS側から受け取るイベントをWKUserContentController
を利用して設定します。
self
に指定したインスタンスはWKScriptMessageHandler
というProtocolに準拠する必要があります。
let contentController = WKUserContentController();
contentController.add(self, name: "increment")
contentController.add(self, name: "decrement")
let webConfiguration = WKWebViewConfiguration()
webConfiguration.userContentController = contentController
self.webView = WKWebView(frame: .zero, configuration: webConfiguration)
WKScriptMessageHandler
に準拠したインスタンスにuserContentController(userContentController:didReceive)
を実装して受け取ったイベントを処理します。
extension ViewController: WKScriptMessageHandler {
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
// nameにWKUserContentController.addで設定したイベント名が入ります
switch message.name {
case "increment":
print("increment")
case "decrement":
print("decrement")
default:
assertionFailure()
}
// bodyにJS側から送られる値が入ります、型はAnyになります
print(message.body)
}
}
JS側ではwebkit
というインスタンスが利用出来るのでこれを利用してiOSへイベントを送ります。
// incrementイベントを送信
try {
webkit.messageHandlers.increment.postMessage(1);
} catch (err) {
console.log(err);
}
// decrementイベントを送信
try {
webkit.messageHandlers.decrement.postMessage(2);
} catch (err) {
console.log(err);
}
iOSからJSへの通信
WKWebViewが提供するevaluateJavaScript
を利用してブラウザへ任意のJSコードを送ることが出来ます。
webView.evaluateJavaScript("console.log('from iOS')") { response, error in
if let error = error {
print(error)
}
print(response)
}
ElmからiOS間の通信
ここまでJSとiOSの通信を書いてきましたので、Elmを少しばかり理解している人ならどうすればいいかはもう分かっていると思います。
Elmのportsを利用してElmとiOSを橋渡しすれば良いだけです。
Elmのサンプルにあるカウンターのコードを少しばかりいじったものを用意します。
port module Main exposing (..)
import Browser
import Html exposing (Html, button, div, text)
import Html.Events exposing (onClick)
type alias Model = Int
main =
Browser.element
{ init = init
, view = view
, update = update
, subscriptions = subscriptions
}
type Msg = Increment | Decrement | Set Model
init : Maybe Model -> ( Model, Cmd msg )
init flags =
case flags of
Just model ->
( model, Cmd.none )
Nothing ->
( 0, Cmd.none )
-- Elmでカウントアップボタンが押されたら、JSへ通知する
port incremented : Model -> Cmd msg
-- Elmでカウントダウンボタンが押されたら、JSへ通知する
port decremented : Model -> Cmd msg
update msg model =
case msg of
Increment ->
let
newModel = model + 1
in
( newModel, incremented newModel )
Decrement ->
let
newModel = model - 1
in
( newModel, decremented newModel )
Set newModel ->
( newModel, Cmd.none )
-- JSから通知を受け取ったら、その値でModelを更新する
port fixModel : (Model -> msg) -> Sub msg
subscriptions model =
fixModel Set
view model =
div []
[ button [ onClick Decrement ] [ text "-" ]
, div [] [ text (String.fromInt model) ]
, button [ onClick Increment ] [ text "+" ]
]
JS側のportsのコードにiOSと連携する処理を書いていきます。
app.ports.incremented.subscribe((data) => {
try {
webkit.messageHandlers.increment.postMessage(data);
} catch (err) {
console.log(err);
}
});
app.ports.decremented.subscribe((data) => {
try {
webkit.messageHandlers.decrement.postMessage(data);
} catch (err) {
console.log(err);
}
});
iOS側からはElmのPortsにイベントを送る処理を書いていきます。
@IBAction func onTapButton(sender: UIButton) {
let js = "app.ports.fixModel.send(10);"
self.webView.evaluateJavaScript(js) { _, error in
if let error = error {
print(error)
}
}
}
記事にのせたコードだと全容が掴み難いかもしれませんので、全体のコードをgistにおいておきました。
興味のある方は御覧ください。
https://gist.github.com/k-motoyan/c4be847b2b36060f2cbb410df7076c20