RAKSUL TechBlog

ラクスルグループのエンジニアが技術トピックを発信するブログです

ハコベルカーゴ iOSアプリ WKWebViewへ移行した話

こんにちは。ハコベルカーゴの開発を担当している貞元です。

ハコベルカーゴでは、ドライバー向けのiOS・Androidアプリがあり、どちらも主にWebViewを使用しています。 iOSアプリではUIWebViewを使用していたのですが、こちらは非推奨となり更新できなくなるためため、WKWebViewへ移行した内容を紹介します。 2020年11月時点では、UIWebViewを使用したアップデート期限は2020年末以降に延長されているため、正式な期限はAppleのニュースをご確認ください。 なお、iOSアプリ開発の経験は豊富ではないため、定番と異なる点などあるかもしれません。ご了承ください。

環境

  • Xcode 12.0.1
  • Swift 4

対応内容

1. WebKit.frameworkを追加

WKWebViewはWebKit.frameworkに含まれているため、WebKit.frameworkを追加します。

対象のTARGETSを選択し、「General」→「Frameworks, Libraries, and Embedded Content」の「+」をクリックし、「WebKit.framework」を追加

2. StoryBoardのUIWebViewをWKWebKitへ置き換え

既存のUIWebViewはStoryBoardに配置し使用していました。 そのため、WKWebViewも同じくStoryBoardへ配置し使用します。 なお、WKWebViewをiOS8から使用できますが、StoryBoardを使用する場合はiOS11以上でないとbuildできないため、iOS Development TargetをiOS 11.0以上へ変更する必要があります。

UIWebViewをWKWebViewへ置き換え

NOTE: dataDetectorTypesについて

WKWebViewにはUIWebViewと同様にdataDetectorTypesが存在します。 ただ、StoryBoardを使用した場合はコード上で変更しても適用されないため、こちらで設定が必要です。

3. Delegateを書き換え

WKWebViewには、WKUIDelegate, WKUIDelegateの2種類のDelegateが存在します。 UIWebViewのUIWebViewDelegateはWKNavigationDelegateへ、JavaScriptでalert, confirm, promptを使用している場合はWKUIDelegateが必要です。

WebKitをインポート

+ import WebKit

Delegateを書き換え

- class WebViewController: UIViewController, UITextViewDelegate, UIWebViewDelegate {
+ class WebViewController: UIViewController, UITextViewDelegate, WKNavigationDelegate, WKUIDelegate {
-     @IBOutlet weak var webview: UIWebView!
+     @IBOutlet weak var webview: WKWebView!

      override func viewDidLoad() {
          super.viewDidLoad()
-         webview.delegate = self
+         webview.navigationDelegate = self
+         webview.uiDelegate = self

UIWebViewDelegate→WKUIDelegate

-     func webView(_ webView: UIWebView, shouldStartLoadWith request: URLRequest, navigationType: UIWebViewNavigationType) -> Bool{
+     func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
-     func webViewDidStartLoad(_ webView: UIWebView){
+     func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
-     func webView(_ webView: UIWebView, didFailLoadWithError error: Error){
+     func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {
-     func webViewDidFinishLoad(_ webView: UIWebView){
+     func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {

NOTE: カスタムURLスキームについて

telやmail、その他カスタムURLスキームはそのままでは動作しません。 そのため、decidePolicyForにて処理する必要があります。

    func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
        let request = navigationAction.request
        let url = navigationAction.request.url

        if url!.absoluteString.hasPrefix("http://") || url!.absoluteString.hasPrefix("https://") {
            switch navigationAction.navigationType {
            case .linkActivated:
                if navigationAction.targetFrame == nil || !navigationAction.targetFrame!.isMainFrame {
                    UIApplication.shared.open(url!, options: [:], completionHandler: nil)
                    decisionHandler(.cancel)
                    return
                }
            case .backForward:
                break
            case .formResubmitted:
                break
            case .formSubmitted:
                break
            case .other:
                break
            case .reload:
                break
            }
        } else {
            if url!.absoluteString.range(of: "//itunes.apple.com/") != nil {
                if UIApplication.shared.responds(to: #selector(UIApplication.open(_:options:completionHandler:))) {
                    UIApplication.shared.open(url!, options: [UIApplicationOpenURLOptionUniversalLinksOnly:false], completionHandler: { (finished: Bool) in
                    })
                } else {
                    UIApplication.shared.open(url!, options: [:], completionHandler: nil)
                }
            } else {
                if UIApplication.shared.canOpenURL(url!) {
                    UIApplication.shared.open(url!, options: [:], completionHandler: nil)
                    decisionHandler(.cancel)
                    return
                }
            }
            decisionHandler(.cancel)
            return
        }
        decisionHandler(.allow)
    }

WKUIDelegate

    /**
     JavaScriptのalertを表示
     */
    func webView(_ webView: WKWebView, runJavaScriptAlertPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping () -> Void) {
        let alertController = UIAlertController(title: "", message: message, preferredStyle: .alert)
        let otherAction = UIAlertAction(title: "OK", style: .default) {
            action in completionHandler()
        }
        alertController.addAction(otherAction)
        present(alertController, animated: true, completion: nil)
    }

    /**
     JavaScriptのconfirmを表示
     */
    func webView(_ webView: WKWebView, runJavaScriptConfirmPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping (Bool) -> Void) {
        let alertController = UIAlertController(title: "", message: message, preferredStyle: .alert)
        let cancelAction = UIAlertAction(title: "Cancel", style: .cancel) {
            action in completionHandler(false)
        }
        let okAction = UIAlertAction(title: "OK", style: .default) {
            action in completionHandler(true)
        }
        alertController.addAction(cancelAction)
        alertController.addAction(okAction)
        present(alertController, animated: true, completion: nil)
    }

    /**
     JavaScriptのpromptを表示
     */
    func webView(_ webView: WKWebView, runJavaScriptTextInputPanelWithPrompt prompt: String, defaultText: String?, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping (String?) -> Void) {
        let alertController = UIAlertController(title: "", message: prompt, preferredStyle: .alert)
        let okHandler: () -> Void = {
            if let textField = alertController.textFields?.first {
                completionHandler(textField.text)
            } else {
                completionHandler("")
            }
        }
        let cancelAction = UIAlertAction(title: "Cancel", style: .cancel) {
            action in completionHandler(nil)
        }
        let okAction = UIAlertAction(title: "OK", style: .default) {
            action in okHandler()
        }
        alertController.addTextField { $0.text = defaultText }
        alertController.addAction(cancelAction)
        alertController.addAction(okAction)
        present(alertController, animated: true, completion: nil)
    }

4. JavaScript呼び出しの変更

UIWebViewにてJavaScriptを呼び出しにstringByEvaluatingJavaScriptを使用していました。 WKWebViewではevaluateJavaScriptを使用します。 こちらは非同期の実行となるため、挙動が変わらないように同期実行用のメソッドを用意し、そちらへ置き換えを行いました。

func evaluateJavaScriptSync(webview:WKWebView, script:String) -> Any? {
    var syncResult:Any? = ""
    var jsCompleted = false

    webview.evaluateJavaScript(script) { (result, error) in
        syncResult = result
        jsCompleted = true
    }
    while !jsCompleted { RunLoop.current.run(mode: .defaultRunLoopMode, before: Date() + 0.1) }

    return syncResult
}

5. Cookie参照の変更

UIWebViewではCookieの参照にHTTPCookieStorage.sharedを使用していました。 しかし、WKWevViewではHTTPCookieStorage.sharedへ反映されず参照できないため、WKWevViewから参照するように変更しています。

webview.configuration.websiteDataStore.httpCookieStore.getAllCookies() {(cookies) in
    for cookie in cookies {
        // ここでCookieを参照
    }
}

まとめ

WKWebViewへの変更期限が近づいています。 iOSアプリ開発の経験は豊富ではないため、調査・変更・テストを繰り返して対応しました。 この記事がWKWebViewの移行の手助けとなると幸いです。