1. 獲取UserAgent為nil
在使用WKWebView獲取userAgent的時候, 如果要全局配置, 使所有的WKWebView都能生效, 我們可能的做法是在AppDelegate中來配置, 但是需要一個WKWebView實例對象, 所以可能是這么寫的:
let webView = WKWebView()
webView.evaluateJavaScript("navigator.userAgent") { (info, error) in
// 獲取默認值
if var userAgent = info as? String {
// 添加自定義的內容
if userAgent.hasSuffix("/ios-app") == false {
userAgent += "/ios-app"
}
// 設置global User-Agent
let dic = ["UserAgent": userAgent]
UserDefaults.standard.register(defaults: dic)
UserDefaults.standard.synchronize()
}
}
這樣, 你會發(fā)現設置一直是無效的, 在回調里打印一下:
print(info)
print(error)
這時發(fā)現獲取到的 info 字段為nil, 并且error信息如下:
Error Domain=WKErrorDomain Code=3 "WKWebView 已失效" UserInfo={NSLocalizedDescription=WKWebView 已失效}
個人猜測: 這是因為, WKWebView的evaluateJavaScript方法是異步執(zhí)行的, 當WKWebView回調這個方法的時候, 其實例對象已經從內存中釋放了, 所以導致回調出錯.
我做了如下驗證, 在回調方法里輸出webView實例對象:
let webView = WKWebView()
webView.evaluateJavaScript("navigator.userAgent") { (info, error) in
print(info)
print(error)
print(webView)
// 獲取默認值
if var userAgent = info as? String {
// 添加自定義的內容
if userAgent.hasSuffix("/ios-app") == false {
userAgent += "/ios-app"
}
// 設置global User-Agent
let dic = ["UserAgent": userAgent]
UserDefaults.standard.register(defaults: dic)
UserDefaults.standard.synchronize()
}
}
會發(fā)現輸出是info有值, 而error為nil, webView有值, 又正常了, 有人說了,看樣子不是這個問題! 真的么? 仔細想一下會發(fā)現, 在webView的方法回調閉包里使用了webView實例, 會發(fā)生什么? 對, 循環(huán)引用! webView實例此時不為nil, 這也驗證了, 如果webView實例正常的話, 獲取結果是不會有誤的! 繼續(xù)上面的驗證, 我們弱引用一下:
let webView = WKWebView()
webView.evaluateJavaScript("navigator.userAgent") {[weak webView] (info, error) in
print(info)
print(error)
print(webView)
// 獲取默認值
if var userAgent = info as? String {
// 添加自定義的內容
if userAgent.hasSuffix("/ios-app") == false {
userAgent += "/ios-app"
}
// 設置global User-Agent
let dic = ["UserAgent": userAgent]
UserDefaults.standard.register(defaults: dic)
UserDefaults.standard.synchronize()
}
}
這時, 會發(fā)現: info和webView都為nil, error值為上面那個錯誤!!!
這樣, 基本驗證出現這個問題的原因是: webView 提前釋放了!
但是為了添加這個設置, 而將webView 設為全局變量, 仿佛有點得不償失, 這時可以在使用webView的頁面進行設置, 或者使用UIWebView替換:
// 獲取默認值
if let oldAgent = UIWebView().stringByEvaluatingJavaScript(from: "navigator.userAgent") {
var newAgent = oldAgent
// 添加自定義的內容
if oldAgent.hasSuffix("/artron-cgyc") == false {
newAgent += "/artron-cgyc"
}
// 設置global User-Agent
let dic = ["UserAgent": newAgent]
UserDefaults.standard.register(defaults: dic)
UserDefaults.standard.synchronize()
}
2. 無需傳參時注入的交互協(xié)議無效
在做JS與原生交互的時候, 使用下面方法注入的協(xié)議無效:
let user = WKUserContentController()
// 向js中注入協(xié)議, 作為ios和js交互的依據
user.add(self, name: "appProtocol")
然后在js端使用的時候: 這里不需要傳參數, 直接這么寫的
window.webkit.messageHandlers.appProtocol.postMessage();
這樣, 沒有響應js端的事件!!!
在代理方法中:
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
if message.name == "appProtocol" {
print(message.body)
}
}
一直沒有收到回調!!!
其實, 并不是注入協(xié)議失敗, 這么使用也沒問題, 問題就出在postMessage的參數上, 如果是帶參數的:
window.webkit.messageHandlers.appProtocol.postMessage({info: 'info', num: '123456788', price: '100'});
這么寫, 是完全沒有問題的, 所以如果不需傳參數的話, 可以這么寫:
// 無參數使用
window.webkit.messageHandlers.appProtocol.postMessage({});
給一個空的字典, 就能正常交互了!!!
3. 屏蔽頁面長按手勢
在WKWebView加載的HTML頁面上, 如果長按會彈出一些選擇框, 在文字上長按, 會彈出UIMenuController選擇框:
而在圖片上長按, 會彈出一個alertSheet:
這里可以保存圖片到系統(tǒng)相冊(如果有權限), 或者復制到剪切板. 但是這些需求并不是我們需要, 如何禁止這些行為呢?需要從JS入手, 只需要執(zhí)行下面兩句js即可:
// 禁止圖片長按事件
document.documentElement.style.webkitTouchCallout='none';
//禁止文本長按事件
document.documentElement.style.webkitUserSelect='none';
可以在創(chuàng)建WKWebView的時候注入:
let config = WKWebViewConfiguration()
config.userContentController = WKUserContentController()
let jsStr = """
document.documentElement.style.webkitTouchCallout='none';
document.documentElement.style.webkitUserSelect='none';
"""
let noneSelectJS = WKUserScript(source: jsStr, injectionTime: WKUserScriptInjectionTime.atDocumentEnd, forMainFrameOnly: true)
config.userContentController.addUserScript(noneSelectJS)
let web = WKWebView(frame: .zero, configuration: config)
也可以在頁面加載完成后的代理方法中執(zhí)行:
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
// 結束加載
let jsStr = """
document.documentElement.style.webkitTouchCallout='none';
document.documentElement.style.webkitUserSelect='none';
"""
webView.evaluateJavaScript(jsStr) { (info, error) in
}
}
4. 頁面中出現第三方的廣告懸浮框(Ta)
在加載的HTML頁面中, 無端出現一個廣告的懸浮框:
打開之后是這樣的:
而且只會在移動4G網絡下才會出現, 其實這是移動的流量劫持, 強加的廣告推廣叉寂,目前網上有一些解決方式蹋绽,常用的有:
- 使用IP地址訪問:將域名改為IP地址訪問數據;
- 使用HTTPS愧薛;
- 前端的小伙伴想想辦法
其他的可參考這篇文章 iOS 客戶端對于運營商劫持的一點點對抗方式
兵钮。
5. 顯示HTML頁面不是最新的內容
在聯調的時候, 前端的同學改了一些東西, 例如頁面的布局, 顯示元素, 或者js方法, 而APP端沒反應!!!
這是因為, WKWebView有緩存, 為了保證每次加載的都是最新的頁面, 可以在加載的鏈接后面加上一個時間戳, 例如你的HTML地址為:
http://your host name/test/20171127.html
一般使用是這樣的:
let urlStr = "http://your host name/test/20171127.html"
if let url = URL(string: urlStr) {
let request = URLRequest(url: url)
webView.load(request)
}
這樣的話是有緩存, 加載一次之后, 再去加載也不是最新的頁面, 可以這樣使用:
let time = Date().timeIntervalSince1970
let urlStr = "http://your host name/test/20171127.html?_t=\(time)"
if let url = URL(string: urlStr) {
let request = URLRequest(url: url)
webView.load(request)
}
這樣每次加載的時候都會是最新的, 當然弊端就是, 每次都會耗費一些額外的流量.
6. 當前頁面無導航時不能填充狀態(tài)欄(iOS11+ 會下移狀態(tài)欄的高度)
在頁面無導航的情況下耐版,系統(tǒng)會自動調節(jié)滾動視圖的contentInset全景,使其視圖永遠處于狀態(tài)欄之下,但是如果我們想讓滾動視圖的Y坐標從屏幕頂端(狀態(tài)欄)開始朋贬,我們都知道怎么修改凯楔,但是 WKWebView不是繼承自UIScrollView 的,所以不能直接設置锦募,可以這么寫:
if (@available(iOS 11.0, *)) {
self.webView.scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
} else {
// Fallback on earlier versions
self.automaticallyAdjustsScrollViewInsets = NO;
}
7. Attempt to add script message handler with name 'action_toiOS' when one already exists.
這個閃退發(fā)生在與 JS 進行交互摆屯,使用下面的方法注冊協(xié)議時:
let user = WKUserContentController()
// 向js中注入協(xié)議, 作為ios和js交互的依據
user.add(self, name: "action_toiOS")
如果重復注冊了相同名稱的協(xié)議,就會發(fā)生閃退糠亩,所以在使用完webView的時候虐骑,一定要記得移除已注冊的協(xié)議:
self.wkWeb.configuration.userContentController.removeScriptMessageHandler(forName: "action_toiOS")
8. Invalid parameter not satisfying: targetNod
app首頁使用 WKWebView 來承載的內容,在啟動時赎线,如果添加了引導頁/廣告頁廷没,這時如果有手勢操作,例如在出現廣告頁時點擊屏幕垂寥,就會閃退颠黎,并在控制臺輸出:
Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid parameter not satisfying: sourceNode'
*** First throw call stack:
(0x1d7131ea4 0x1d6301a50 0x1d7047a2c 0x1d7b361d0 0x203ed4984 0x203ed34e4 0x203edb728 0x203ee144c 0x203edd398 0x203edd2cc 0x203edd09c 0x204308cb4 0x2042e7fcc 0x2043b6e38 0x2043b9830 0x2043b2320 0x1d70c20e0 0x1d70c2060 0x1d70c1944 0x1d70bc810 0x1d70bc0e0 0x1d9335584 0x2042ccc00 0x1042c47a4 0x1d6b7abb4)
添加一個全局斷點,調試發(fā)現崩潰信息為
Assertion failure in -[UIGestureGraphEdge initWithLabel:sourceNode:targetNode:directed:], /BuildRoot/Library/Caches/com.apple.xbs/Sources/UIKitCore/UIKit-3698.94.14/Source/GestureGraph/UIGestureGraphEdge.m:24
查了些資料滞项,沒找到具體原因狭归,但是了解到和 +load方法有關,我是在 +load 方法內初始化的廣告頁的信息文判,把這些放在 didFinishLaunchingWithOptions 進行初始化过椎,就不會有這個問題;
解決:
將廣告/引導頁視圖的初始化放在 didFinishLaunchingWithOptions 方法內戏仓。