ネイティブアプリなんだけどネイティブのコードは極力書かず、 WebViewを使ってhtml+JavaScriptを使ってアプリを組みたい。 今回はiOS, Swiftで作ってみる。

コード

  • ‘16/11/30: UIWebViewからWKWebViewに変更

プロジェクトの作成

Xcodeでプロジェクトを新規作成する。

  • Xcodeを起動して現れるダイアログで Create a new Xcode project > iOS > Application > Single View Application を選び、Nextボタンを押す
  • 「Choose options for your new project」ダイアログで Product Name などの情報を入力
    • 使用言語がObjective-CとSwiftから選べる
  • プロジェクトを保存するパスを指定

Storyboardを使わないようにする

単にWebViewを全画面に配置するだけの単純なレイアウトなのでStoryboardは必要ない。 なのでXcodeでSwiftのプロジェクトを作った時に自動的に作られるMain.storyboardを使わないようにする。

  • Xcodeでプロジェクトのターゲット > General > Deployment Info > Main Interface を空にする。
  • Main.storyboardを削除する
  • ViewControllerが呼び出されなくなるので、自前で呼び出す
class AppDelegate: UIResponder, UIApplicationDelegate {
  var window: UIWindow?
  var navigationController: UINavigationController?

  func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
    window = UIWindow(frame: UIScreen.main.bounds)

    let viewController: ViewController = ViewController()
    window!.rootViewController = viewController
    window!.makeKeyAndVisible()

    return true
  }
  ...

WebViewを全画面の大きさで追加

ViewControllerで全画面のWebViewを追加する。

class ViewController: UIViewController, WKNavigationDelegate {
  let webView: WKWebView = WKWebView()

  override func viewDidLoad() {
    super.viewDidLoad()

    webView.frame = view.bounds
    webView.navigationDelegate = self
    view.addSubview(webView)
  }
  • WKWebViewからのイベントを処理するため、WKNavigationDelegateを継承してやる
  • webViewframeをコントローラ自体のview.boundsにしてやると全画面になる

htmlの表示

htmlは外部httpサーバから取ってくることもできるが、ここではリソースとしてアプリ内部に持ち、 それをWebViewで表示することにする。

  override func viewDidLoad() {
    ...
    //let url = URL(string: "https://www.example.com/")!  // 外部のURLを読み込む場合(httpの場合Info.plistにNSAppTransportSecurityの指定が必要)
    let url = Bundle.main.url(forResource: "index", withExtension: ".html")!
    let urlRequest = URLRequest(url: url)
    webView.load(urlRequest)
  }
  • プロジェクトにResourcesとかいうグループを作り、その中にhtmlファイルを追加する (ここではindex.htmlにした)
  • プロジェクトの設定のBuild Phases > Copy Bundle Resourcesで追加する (ドラッグドロップでプロジェクトにファイルを追加した場合には自動的に登録される?ので別途行う必要はないみたい)
  • Bundle#url(forResource:withExtension)でリソースのパスを取得
  • WKWebView#loadでWebViewに読み込む

読み込むファイルをサブディレクトリの中に入れたい場合、FinderからXcodeのResources内にサブフォルダをドラッグドロップして、Added foldersに「Create folder references」を選ぶ

htmlから画像、JavaScript、CSSを読み込む

html内で相対パスで書けばリソース内のファイルが自動的に読み込まれる

<link rel="stylesheet" type="text/css" href="main.css" />
<img src="hoge.png" />
<script type="text/javascript" src="main.js"></script>

JavaScriptとネイティブの連携

JavaScriptからネイティブを呼び出す

JavaScriptからネイティブに対してなにか起動するにはURLをリクエストして、 WKNavigationDelegate#webView(_:decidePolicyFor:decisionHandler:)が呼び出されるのを利用する。

スキーマをhttp://https://じゃなくて独自のものにしておくことで判定する。 ここではnative://などとしてみる。

html(JavaScript)側:

<p><a href="native://foo/bar.baz">Push me!</a></p>

ネイティブ(Swift)側:

  let kScheme = "native://";

  func webView(_ webView: WKWebView,
               decidePolicyFor navigationAction: WKNavigationAction,
               decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
    var policy = WKNavigationActionPolicy.allow
    if let url = navigationAction.request.url?.absoluteString {
      if url.hasPrefix(kScheme) {
        evaluateJs("addTextNode('\(url) ');")
        policy = WKNavigationActionPolicy.cancel  // ページ遷移を行わないようにcancelを返す
      }
    }
    decisionHandler(policy)
  }
  • 呼び出されたネイティブ側では、スキーム以外のURLの残り部分を使って自由に処理すればよい
  • 他にはMessage Handlerというものを使用する方法があるようです(WKUserContentController#add(_:name:)

ネイティブからJavaScriptを呼び出す

WKWebView#evaluateJavaScript(_:completionHandler:)を使用する:

  func evaluateJs(_ script: String) {
    webView.evaluateJavaScript(script, completionHandler: {(result: Any?, error: Error?) in
      print("JS result=\(result), error=\(error)")
    })
  }
  • 毎回evalすることになるし全部文字列にしないといけないのがアレだけど…
  • JavaScriptの実行結果を扱いたい場合にはcompletionHandlerを指定してやる(省略可能)

実行例

スクリーンショット

関連