さいしょに
iOSでWebページを表示するいわゆるガワアプリの作成でつまずいたことのまとめです。
とりあえず動作はしていますがこの解決法がいいのかはわかりません...
今回はiOS11以降はWKWebView、iOS10以前のバージョンではUIWebViewを利用しました。
POSTリクエスト
WKWebviewでは割と有名な話なのかしれませんがここに記載されているように、POSTのhttpBodyがnilになるというバグがあります。
iOS11以降
iOS11以降は解決しているようで下記のようにiOS11では普通にPOSTできます。
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以前の場合はここにあるように下記の方法でURLSession経由で表示できるそうなのですが、今回表示するWebページではログイン処理があり通信時にWKWebViewとURLSessionでセッションIDを共有しないといけないのですが、後述しますがセッションIDの取得ができずこの方法が使えませんでした。
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でするようにしました。)
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の場合
ここに記載されているようにWKWebViewでJavaScriptのアラートを表示する際は、下記のようにWKUIDelegateを設定する必要があります。
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の場合
ここを参考に下記のような処理で動作しました。
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の場合
ここを参考に下記の処理で動作しましたが、この方法でいいのかは...(JavaScriptを書き換えてopen/closeの処理を取得しています)
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以降しか使えません。
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ページでは含まれていなかったので下記の方法では取得できませんでした。
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は取得できませんでした。
/// 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を取得することができました。
/// 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以降では下記で設定できると思ったのですができず。
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)
}
}
ここを参考に下記の方法で設定できました。
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の場合は下記の方法で設定できました。
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>
<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の場合
下記の方法で取得できました。
webView.evaluateJavaScript("document.test_form.test_name1.value") { (result, error) in
if let text = result as? String {
print(text)// test_name1の値
}
}
即時関数を使えば下記のように複数の値も取得できました。
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の場合
下記の方法で取得できました。
if let text = webView.stringByEvaluatingJavaScript(from: "document.test_form.test_name1.value") {
print(text)// test_name1の値
}
stringByEvaluatingJavaScript
は返り値がString型なので上記のような即時関数を使用すると空文字が返ってきました。
下記のように配列を連結して文字列に変換すれば複数の値の取得ができました。
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の値
}
もしくは下記のように取得したい数分呼び出せば複数の値も取得可能です。
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の場合
ここを参考に下記の方法でできました。
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になってしまいます。
WKWebView().evaluateJavaScript("navigator.userAgent") { (result, error) in
if let userAgent = result as? String {
self.webView.customUserAgent = "\(userAgent)suffix"
}
}
##UIWebViewの場合
ここを参考に下記の方法でできました。
if let userAgent = UIWebView().stringByEvaluatingJavaScript(from: "navigator.userAgent") {
UserDefaults.standard.register(defaults: ["UserAgent":"\(userAgent)suffix"])
}
ポイントは使用するWebViewを生成する前に上記の処理を行うことです。
#Web側からアプリのメソッドを呼び出す(2018/12/13追記)
Web側から任意のタイミングでswiftのメソッドを呼び出せないかと悩んだ結果、下記の方法でどうにかできました。
WKWebViewしか使わない場合はここのようにWKUserContentController
を利用すればできるようです。
ここを参考に無理矢理ですがなんとか動くものはできました。
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文字列にすれば複数の値も渡すことができます。
let value = url.absoluteString.replacingOccurrences(of: "testapp://", with: "")
testAction(value: value)
private func testAction(value: String) {
}
とりあえず動きましたが、これが良い実装とは思えません...
さいごに
とりあえずは動作するものができたのですが、この実装方法でいいのかは不明です。
どなたかもっといい方法を知っている方いればぜひ教えてください。
作った感想としてはWeb側の回収が可能であればできるだけアプリ専用のものに作り直してもらった方がいいです。アプリ側だけで無理矢理やるのは保守とか考えるとかなり無茶な気がします。(まあ、大抵そんな簡単に改修できないと思いますが...)
複雑なサイトでガワアプリなんて作るもんじゃないなと思いました。
参考
- iOSのWKWebViewの3つのつまずきポイントと解決方法
- WKWebView の POST の httpBody が nil になる
- WKWebViewでJavaScript(Alert,Confirm,Prompt)の処理
- WKWebViewで躓いた10つのまとめ
- iOS 11 WKWebView 3大新機能 (WWDC 2017)
- WKWebViewで新しいウィンドウ(タブ)を開く
- iOS UIWebView Handle window.open/window.close PopUp Page
- WKWebViewで表示したページのCookieを取得する
- ガワネイティブのCookie同期
- UIWebViewにCookieを設定する方法
- WKWebViewでのSessionの共有
- Swift3 でWKWebViewからpostしたフォームの値を取得する
- iOS WKWebView ネイティブとローカルJavascript連携
- WebView の UserAgent を取得設定する iOS 9対応
- iOS 12のWKWebViewでcustomUserAgentの設定に失敗する問題の対処法
- [XCODE] UIWebViewでJS -> Native、Native -> JS連携を行う方法