iOS(swift)ガワアプリの作成で色々苦労した話

# さいしょに
iOSでWebページを表示するいわゆるガワアプリの作成でつまずいたことのまとめです。
とりあえず動作はしていますがこの解決法がいいのかはわかりません...

今回はiOS11以降はWKWebView、iOS10以前のバージョンではUIWebViewを利用しました。

# POSTリクエスト
WKWebviewでは割と有名な話なのかしれませんが[ここ](https://qiita.com/peka2/items/5fb68837b1145613e535)に記載されているように、POSTのhttpBodyがnilになるというバグがあります。

## iOS11以降
iOS11以降は解決しているようで下記のようにiOS11では普通にPOSTできます。

```Swift
var request = URLRequest(url: URL(string: "https://www.xxxxx")!)
request.httpMethod = "POST"
bodyData = "A=Value"
request.httpBody = bodyData.data(using: .utf8)!
webView.load(request)//WKWebView
```

## iOS10以前
iOS10以前の場合は[ここ](https://qiita.com/shunkitan/items/a23b0b109f434a28ab8b)にあるように下記の方法でURLSession経由で表示できるそうなのですが、今回表示するWebページではログイン処理があり通信時にWKWebViewとURLSessionでセッションIDを共有しないといけないのですが、後述しますがセッションIDの取得ができずこの方法が使えませんでした。

```Swift
var request = URLRequest(url: URL(string: "https://www.xxxxx")!)
request.httpMethod = "POST"
bodyData = "A=Value"
request.httpBody = bodyData.data(using: .utf8)!
let task = URLSession.shared.dataTask(with: request) { data, response, error in
	if let data = data, let response = response {
		DispatchQueue.main.async {
			webView.loadHTMLString(html, baseURL: URL(string: "https://www.xxxx")!)//WKWebView
		}
	} 
}
task.resume()
```

解決策としてなくなくUIWebViewを使いました。（iOS10以前では表示は全てUIWebViewでするようにしました。）

```Swift
var request = URLRequest(url: URL(string: "https://www.xxxxx")!)
request.httpMethod = "POST"
bodyData = "A=Value"
request.httpBody = bodyData.data(using: .utf8)!
webView.loadRequest(request)//UIWebView
```

# JavaScriptのアラート表示
## WKWebViewの場合
[ここ](https://qiita.com/ShingoFukuyama/items/5d97e6c62e3813d1ae98)に記載されているようにWKWebViewでJavaScriptのアラートを表示する際は、下記のようにWKUIDelegateを設定する必要があります。

```Swift
extension ViewController: WKUIDelegate {
    
    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)
    }
    
    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)
    }
    
    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 in
            if let textField = alertController.textFields?.first {
                completionHandler(textField.text)
            } else {
                completionHandler("")
            }
        }
        let cancelAction = UIAlertAction(title: "Cancel", style: .cancel) {
            action in completionHandler("")
        }
        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)
    }
}

```

## UIWebViewの場合
特に何も設定せずに表示されました。

# 別ウィンドウでのページ遷移
Webページを表示する際、window.open/closeが反応しないのでそれぞれ自前で処理してやる必要があります。

## WKWebViewの場合
[ここ](https://qiita.com/m__ike_/items/f2f9edf1dd9dfad70f13#_reference-d986a141d4a5d1c3f4b2)を参考に下記のような処理で動作しました。

```Swift
extension ViewController: WKNavigationDelegate {
    func webView(_ webView: WKWebView,
                 createWebViewWith configuration: WKWebViewConfiguration,
                 for navigationAction: WKNavigationAction,
                 windowFeatures: WKWindowFeatures) -> WKWebView?
    {
        if navigationAction.targetFrame?.isMainFrame != true {
            // 別Window表示の場合
            // 開く処理
            let newWebView = WKWebView(frame: webView.frame,
                                       configuration: configuration)
            newWebView.translatesAutoresizingMaskIntoConstraints = true
            newWebView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
            newWebView.load(navigationAction.request)
            newWebView.uiDelegate = self
            webView.superview?.addSubview(newWebView)
            return newWebView
        }
        
        return nil
    }
    
    func webViewDidClose(_ webView: WKWebView) {
    	 // 閉じる処理
        webView.removeFromSuperview()
    }
}
```

## UIWebViewの場合
[ここ](http://brainwashinc.com/2014/01/06/ios-uiwebview-handle-window-openwindow-close-popup-page/)を参考に下記の処理で動作しましたが、この方法でいいのかは...(JavaScriptを書き換えてopen/closeの処理を取得しています)

```Swift
extension ViewController: UIWebViewDelegate {
    
    func webViewDidFinishLoad(_ webView: UIWebView) {
        // window.open/colse用に書き換え
        webView.stringByEvaluatingJavaScript(from: "window.close = function () {window.location.assign('close://' + window.location);};")
        webView.stringByEvaluatingJavaScript(from: "window.open = function (url, d1, d2) {window.location = 'open://' + url;};")
    }
    
    func webView(_ webView: UIWebView,
                 shouldStartLoadWith request: URLRequest,
                 navigationType: UIWebViewNavigationType) -> Bool {
        if isWindowOpen(url: request.url!)  {
            // 別Window表示の場合
            // 開く処理
            let newWebView = UIWebView(frame: webView.frame)
            newWebView.translatesAutoresizingMaskIntoConstraints = true
            newWebView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
            newWebView.scalesPageToFit = true
            newWebView.delegate = self
            let newRequest = URLRequest(url: convertWindowOpenUrl(url: request.url!))
            newWebView.loadRequest(newRequest)
            webView.superview?.addSubview(newWebView)
            return false
        }
        
        if isWindowClose(url: request.url!) {
            // 閉じる処理
            webView.removeFromSuperview()
            return false
        }
        
        return true
    }
    
    private func isWindowOpen(url: URL) -> Bool {
        if url.scheme == "open"  {
            return true
        }
        return false
    }
    
    private func isWindowClose(url: URL) -> Bool {
        if url.scheme == "close"  {
            return true
        }
        return false
    }
    
    private func convertWindowOpenUrl(url: URL) -> URL {
        //open://で書き換えられているURLをここで元に戻す
        return URL(string: "https://wwww.xxxxx")!
    }
}
```

# セッションIDの保持
今回のアプリにはWebページでログイン処理があり、アプリ再起動時にもログイン状態を保持する必要がありました。アプリを終了した際はセッションIDが破棄されるのでこれをアプリ側で保持して、再起動時の初回リクエスト時に設定してやる必要があります。
ここが一番苦労しました...

## セッションIDの取得
### iOS11以降
iOS11以降の場合は簡単に取得することができました。ログイン処理後のページ遷移時に下記のように処理。
残念ながらこのhttpCookieStoreはiOS11以降しか使えません。

```Swift
extension ViewController: WKNavigationDelegate {
    func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
        let cookieStore = webView.configuration.websiteDataStore.httpCookieStore
        cookieStore.getAllCookies() { (cookies) in
            for cookie in cookies {
                if cookie.domain == "www.xxxxxx" &&
                    cookie.name == "SESSION_ID" {
                    // UserDefaultsに保存
                    let cookieData = NSKeyedArchiver.archivedData(withRootObject: cookie)
                    UserDefaults.standard.set(cookieData, forKey: "Cookie")
                    UserDefaults.standard.synchronize()
                }
            }
        }
    }
}
```

### iOS10以前
レスポンスヘッダにセッションIDがあれば下記の方法で取得できるようですが、今回のWebページでは含まれていなかったので下記の方法では取得**できません**でした。

```Swift
extension ViewController: WKNavigationDelegate {
    func webView(_ webView: WKWebView,
                 decidePolicyFor navigationResponse: WKNavigationResponse,
                 decisionHandler: @escaping (WKNavigationResponsePolicy) -> Void) {
        let response = navigationResponse.response as! HTTPURLResponse
        let cookies = HTTPCookie.cookies(withResponseHeaderFields: response.allHeaderFields as! [String : String],
                                         for: response.url!)
        for cookie in cookies {
            if cookie.domain == "www.xxxxxx" &&
                cookie.name == "SESSION_ID" {
                // UserDefaultsに保存
                let cookieData = NSKeyedArchiver.archivedData(withRootObject: cookie)
                UserDefaults.standard.set(cookieData, forKey: "Cookie")
                UserDefaults.standard.synchronize()
            }
        }
        decisionHandler(.allow)
    }
}
```

HTTPCookieStorageというものがあるようで、下記の方法も試してみましたがセッションIDは取得**できません**でした。

```Swift
/// viewDidloadでHTTPCookieStorage.shared.cookieAcceptPolicy = .always設定

extension ViewController: WKNavigationDelegate {
    func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
        if let cookies = HTTPCookieStorage.shared.cookies(for: URL(string: "https://www.xxxx")!) {
            for cookie in cookies {
                if cookie.domain == "www.xxxxxx" &&
                    cookie.name == "SESSION_ID" {
                    // UserDefaultsに保存
                    let cookieData = NSKeyedArchiver.archivedData(withRootObject: cookie)
                    UserDefaults.standard.set(cookieData, forKey: "Cookie")
                    UserDefaults.standard.synchronize()
                }
            }
        }
    }
}
```

悩んだ末にUIWebViewで下記の方法でセッションIDを取得することができました。

```Swift
/// viewDidloadでHTTPCookieStorage.shared.cookieAcceptPolicy = .always設定

extension ViewController: UIWebViewDelegate {
    func webView(_ webView: UIWebView,
                 shouldStartLoadWith request: URLRequest,
                 navigationType: UIWebViewNavigationType) -> Bool {
        if let cookies = HTTPCookieStorage.shared.cookies(for: URL(string: "https://www.xxxx")!) {
            for cookie in cookies {
                if cookie.domain == "www.xxxxxx" &&
                    cookie.name == "SESSION_ID" {
                    // UserDefaultsに保存
                    let cookieData = NSKeyedArchiver.archivedData(withRootObject: cookie)
                    UserDefaults.standard.set(cookieData, forKey: "Cookie")
                    UserDefaults.standard.synchronize()
                }
            }
        }
        return true
    }
}
```

## セッションIDの設定
保持したセッションIDをアプリ再起動時に設定する処理も色々苦労しました...
### WKWebViewの場合
iOS11以降では下記で設定できると思ったのですが**できず**。

```Swift
override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
        guard let cookie = getCookie() else {
            return
        }
        let cookieStore = webView.configuration.websiteDataStore.httpCookieStore
        cookieStore.setCookie(cookie) {
            var request = URLRequest(url: URL(string: "https://www.xxxxx")!)
            request.allHTTPHeaderFields = ["Cookie":"SESSIONID=\(cookie.value)"]
            self.webView.load(request)
        }
    }
```

[ここ](https://qiita.com/nikadon/items/485b02d412d9d21adf6c)を参考に下記の方法で設定できました。

```Swift
override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
        guard let cookie = getCookie() else {
            return
        }
        let userContentController = WKUserContentController()
        let script = "document.cookie='_session_id=\(cookie.value); domain=\(cookie.domain); path=\(cookie.path);"
        let cookieScript = WKUserScript(source: script,
                                        injectionTime: .atDocumentStart,
                                        forMainFrameOnly: false)
        userContentController.addUserScript(cookieScript)
        
        let configuration = WKWebViewConfiguration()
        configuration.userContentController = userContentController
        webView = WKWebView(frame: view.bounds, configuration: configuration)
        var request = URLRequest(url: URL(string: "https://www.xxxxx")!)
        request.allHTTPHeaderFields = ["Cookie":"SESSIONID=\(cookie.value)"]
        webView.load(request)
    }
```

### UIWebViewの場合
UIWebViewの場合は下記の方法で設定できました。

```Swift
override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
        HTTPCookieStorage.shared.cookieAcceptPolicy = .always
        guard let cookie = getCookie() else {
            return
        }
        HTTPCookieStorage.shared.setCookie(cookie)
        var request = URLRequest(url: URL(string: "https://www.xxxxx")!)
        request.allHTTPHeaderFields = ["Cookie":"SESSIONID=\(cookie.value)"]
        webView.loadRequest(request)
    }
```

#JavaSriptでの値取得(2018/11/12追記)
Webページのformの値などを取得したい場合があります。
下記のようなWebページのformの値の取得法について記載します。

```HTML
<html>
	<body>
		<h1>TEST</h1>
		<form action="/test" name="test_form" method="POST">
			<input type="text" name="test_name1">
			<input type="text" name="test_name2">
			<input type="submit">
		</form>
	</body>
</html>
```

##WKWebViewの場合
下記の方法で取得できました。

```Swift
webView.evaluateJavaScript("document.test_form.test_name1.value") { (result, error) in
        if let text = result as? String {
                print(text)// test_name1の値
        }
}
```

即時関数を使えば下記のように複数の値も取得できました。

```Swift
let script =
        """
        (function () {
            var list = [];
            list.push(document.test_form.test_name1.value);
            list.push(document.test_form.test_name2.value);
            return list;
        })();
        """
webView.evaluateJavaScript(script) { (result, error) in
        if let list = result as? [String] {
                print(list)
        }
}
```

##UIWebViewの場合
下記の方法で取得できました。

```Swift
if let text = webView.stringByEvaluatingJavaScript(from: "document.test_form.test_name1.value") {
        print(text)// test_name1の値
}
```

`stringByEvaluatingJavaScript`は返り値がString型なので上記のような即時関数を使用すると空文字が返ってきました。

下記のように配列を連結して文字列に変換すれば複数の値の取得ができました。

```Swift
let script =
        """
        (function () {
            var list = [];
            list.push(document.test_form.test_name1.value);
            list.push(document.test_form.test_name2.value);
            return list.join(',');
        })();
        """
if let text = webView.stringByEvaluatingJavaScript(from: script) {
        print(text)// test_name1の値,test_name2の値
}
```

もしくは下記のように取得したい数分呼び出せば複数の値も取得可能です。

```Swift
if let text = webView.stringByEvaluatingJavaScript(from: "document.test_form.test_name1.value") {
        print(text)// test_name1の値
}
if let text = webView.stringByEvaluatingJavaScript(from: "document.test_form.test_name2.value") {
        print(text)// test_name2の値
}
```

JavaScriptでアクセスできる値であれば上記の方法で取得できるはずです。Javaサーブレットなどで保持している値はJavaScriptでアクセスできないので取得できないと思います。（たぶん...）

#UserAgentの書き換え(2018/12/13追記)
Web側でアプリからのアクセスであることを判定するために既存のUAを取得して末尾に何か文字列を足してUAを書き換える方法を紹介します。UAの変更はすぐにできたのですが、既存のUAの末尾に足すのに少し手間取ったので...

##WKWebViewの場合
[ここ](https://qiita.com/beryu/items/d1f040ab500b57ec7cb5)を参考に下記の方法でできました。

```Swift
var dummyWebView: WKWebView? = WKWebView()
dummyWebView.evaluateJavaScript("navigator.userAgent") { (result, error) in
	dummyWebView = nil 
	if let userAgent = result as? String {
		self.webView.customUserAgent = "\(userAgent)suffix"
	}
}
```

ポイントは使用するWebViewとは別のWebViewを生成して取得することです。`navigator.userAgent` をしてしまうとそのWebViewのuserAgentの書き換えができなくなります。あとは変数を宣言し、クロージャの処理が終わるまで保持することです。
下記のような方法だとクロージャの処理が終わるまでにWebViewが解放されてしまいresultがnilになってしまいます。

```Swift
WKWebView().evaluateJavaScript("navigator.userAgent") { (result, error) in
	if let userAgent = result as? String {
		self.webView.customUserAgent = "\(userAgent)suffix"
	}
}
```

##UIWebViewの場合
[ここ](http://koze.hatenablog.jp/entry/2015/06/12/230000)を参考に下記の方法でできました。

```Swift
if let userAgent = UIWebView().stringByEvaluatingJavaScript(from: "navigator.userAgent") {
	UserDefaults.standard.register(defaults: ["UserAgent":"\(userAgent)suffix"])
}
```
ポイントは使用するWebViewを生成する前に上記の処理を行うことです。

#Web側からアプリのメソッドを呼び出す(2018/12/13追記)
Web側から任意のタイミングでswiftのメソッドを呼び出せないかと悩んだ結果、下記の方法でどうにかできました。
WKWebViewしか使わない場合は[ここ](https://qiita.com/beaa/items/cfb80790d333c2da252b)のように`WKUserContentController `を利用すればできるようです。

[ここ](https://www.yoheim.net/blog.php?q=20130105)を参考に無理矢理ですがなんとか動くものはできました。

```JavaScript
location.href='testapp://value';
```
Web側でメソッドを呼び出したい箇所で上記の処理を書きます。
アプリ側でUIWebViewなら`func webView(_ webView: UIWebView, shouldStartLoadWith request: URLRequest,navigationType: UIWebViewNavigationType) -> Bool `WKWebViewなら`func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void)`でURLのスキームがtestappか判定し任意のメソッドを呼び出します。
下記のようにurlからvalueの部分を取り出しメソッドを呼び出せば値を渡せます。valueの部分をJSON文字列にすれば複数の値も渡すことができます。

```Swift
let value = url.absoluteString.replacingOccurrences(of: "testapp://", with: "")
testAction(value: value)
private func testAction(value: String) {
    
}
```

とりあえず動きましたが、これが良い実装とは思えません...

# さいごに
とりあえずは動作するものができたのですが、この実装方法でいいのかは不明です。

どなたかもっといい方法を知っている方いればぜひ教えてください。

作った感想としてはWeb側の回収が可能であればできるだけアプリ専用のものに作り直してもらった方がいいです。アプリ側だけで無理矢理やるのは保守とか考えるとかなり無茶な気がします。(まあ、大抵そんな簡単に改修できないと思いますが...)
複雑なサイトでガワアプリなんて作るもんじゃないなと思いました。

# 参考
* [iOSのWKWebViewの3つのつまずきポイントと解決方法](https://qiita.com/shunkitan/items/a23b0b109f434a28ab8b)
* [WKWebView の POST の httpBody が nil になる](https://qiita.com/peka2/items/5fb68837b1145613e535)
* [WKWebViewでJavaScript(Alert,Confirm,Prompt)の処理](https://qiita.com/ShingoFukuyama/items/5d97e6c62e3813d1ae98)
* [WKWebViewで躓いた10つのまとめ](https://qiita.com/ShingoFukuyama/items/eede79a284c3669846e9)
* [iOS 11 WKWebView 3大新機能 (WWDC 2017)](https://qiita.com/ShingoFukuyama/items/c2d33ff3fc36dbdb69a4)
* [WKWebViewで新しいウィンドウ（タブ）を開く](https://qiita.com/m__ike_/items/f2f9edf1dd9dfad70f13#_reference-d986a141d4a5d1c3f4b2)
* [iOS UIWebView Handle window.open/window.close PopUp Page](http://brainwashinc.com/2014/01/06/ios-uiwebview-handle-window-openwindow-close-popup-page/)
* [WKWebViewで表示したページのCookieを取得する](https://qiita.com/6rats/items/2953f87978d0d7169ecf)
* [ガワネイティブのCookie同期](http://takuyaokamoto.hateblo.jp/entry/2017/07/03/013705)
* [UIWebViewにCookieを設定する方法](https://qiita.com/happy_ryo/items/c34dab666f7f0014d00a)
* [WKWebViewでのSessionの共有](https://qiita.com/nikadon/items/485b02d412d9d21adf6c)
* [Swift3 でWKWebViewからpostしたフォームの値を取得する](https://trueman-developer.blogspot.com/2016/10/swift3-wkwebviewpost.html)
* [iOS WKWebView ネイティブとローカルJavascript連携](https://qiita.com/beaa/items/cfb80790d333c2da252b)
* [WebView の UserAgent を取得設定する iOS 9対応](http://koze.hatenablog.jp/entry/2015/06/12/230000)
* [iOS 12のWKWebViewでcustomUserAgentの設定に失敗する問題の対処法](https://qiita.com/beryu/items/d1f040ab500b57ec7cb5)
* [[XCODE] UIWebViewでJS -> Native、Native -> JS連携を行う方法](https://www.yoheim.net/blog.php?q=20130105)