最近公司要使用Hybird混合開(kāi)發(fā),所以就要學(xué)習(xí)一下JS與Swift的交互,以便之后的工作;據(jù)我所知,iOS下JS與原生的交互有很多種具體有:
- 使用UIWebView與WKWebView的代理方法,在JS 中做一次URL跳轉(zhuǎn),然后在Swift中攔截跳轉(zhuǎn)
- 使用WKWebView 的MessageHandler
- 使用系統(tǒng)庫(kù)JavaScriptCore,來(lái)做相互調(diào)用(iOS 7推出的)
- 使用第三方庫(kù)WebViewJavascriptBridge
- 使用第三方cordova庫(kù),以前叫PhoneGap(這是一個(gè)庫(kù)平臺(tái)的庫(kù))
- 使用React Native
本文主要是 寫使用UIWebView與WKWebView的代理方法,在JS 中做一次URL跳轉(zhuǎn),然后在Swift中攔截跳轉(zhuǎn)
的情況
1. 使用的情景
當(dāng)我們?cè)谂cJS交互的接口比較少時(shí),就適用這種情況
2. UIWebView的情景
首先,創(chuàng)建UIWebView,并加載本地HTML
lazy var webView: UIWebView = {[unowned self] in
let view = UIWebView(frame: self.view.bounds)
let htmlURL = Bundle.main.url(forResource: "anran.html", withExtension: nil)
let request = URLRequest(url: htmlURL!)
let request1 = URLRequest(url: URL(string: "https://www.baidu.com")!)
view.scrollView.decelerationRate = UIScrollViewDecelerationRateNormal
view.loadRequest(request)
view.scrollView.bounces = false
view.delegate = self
return view
}()
然后,在HTML里,定義按鈕,來(lái)觸發(fā)調(diào)用原生的方法,然后再將執(zhí)行結(jié)果回調(diào)到j(luò)s 里。
<input type="button" value="點(diǎn)我點(diǎn)我" onclick="scanClick()" />
<input type="button" value="獲取定位" onclick="locationClick()" />
<input type="button" value="修改導(dǎo)航欄顏色" onclick="colorClick()" />
<input type="button" value="分享" onclick="shareClick()" />
<input type="button" value="支付" onclick="payClick()" />
<input type="button" value="動(dòng)次打次" onclick="shake()" />
<input type="button" value="返回" onclick="goBack()" />
function loadURL(url) {
var iFrame;
iFrame = document.createElement("iframe");
iFrame.setAttribute("src", url);
iFrame.setAttribute("style", "display:none;");
iFrame.setAttribute("height", "0px");
iFrame.setAttribute("width", "0px");
iFrame.setAttribute("frameborder", "0");
document.body.appendChild(iFrame);
// 發(fā)起請(qǐng)求后這個(gè)iFrame就沒(méi)用了,所以把它從dom上移除掉
iFrame.parentNode.removeChild(iFrame);
iFrame = null;
}
1.為什么自定義一個(gè)loadURL 方法,不直接使用window.location.href?
答:因?yàn)槿绻?dāng)前網(wǎng)頁(yè)正使用window.location.href加載網(wǎng)頁(yè)的同時(shí),調(diào)用window.location.href去調(diào)用OC原生方法,會(huì)導(dǎo)致加載網(wǎng)頁(yè)的操作被取消掉。
同樣的,如果連續(xù)使用window.location.href執(zhí)行兩次OC原生調(diào)用,也有可能導(dǎo)致第一次的操作被取消掉。所以我們使用自定義的loadURL,來(lái)避免這個(gè)問(wèn)題。
loadURL的實(shí)現(xiàn)來(lái)自關(guān)于UIWebView和PhoneGap的總結(jié)一文。
2.為什么loadURL 中的鏈接,使用統(tǒng)一的scheme?
答:便于在OC 中做攔截處理,減少在JS中調(diào)用一些OC 沒(méi)有實(shí)現(xiàn)的方法時(shí),webView 做跳轉(zhuǎn)。因?yàn)槲以贠C 中攔截URL 時(shí),根據(jù)scheme (即haleyAction)來(lái)區(qū)分是調(diào)用原生的方法還是正常的網(wǎng)頁(yè)跳轉(zhuǎn)。然后根據(jù)host(即//后的部分getLocation)來(lái)區(qū)分執(zhí)行什么操作。
3.為什么自定義一個(gè)asyncAlert方法?
答:因?yàn)橛械腏S調(diào)用是需要OC 返回結(jié)果到JS的。stringByEvaluatingJavaScriptFromString是一個(gè)同步方法,會(huì)等待js 方法執(zhí)行完成,而彈出的alert 也會(huì)阻塞界面等待用戶響應(yīng),所以他們可能會(huì)造成死鎖。導(dǎo)致alert 卡死界面。如果回調(diào)的JS 是一個(gè)耗時(shí)的操作,那么建議將耗時(shí)的操作也放入setTimeout的function 中。
最后,攔截URL也就是自定義的協(xié)議,UIWebView 有一個(gè)代理方法,可以攔截到每一個(gè)鏈接的Request。return true,webView 就會(huì)加載這個(gè)鏈接;return false,webView 就不會(huì)加載這個(gè)連接,我們就在這個(gè)攔截的代理方法中處理自己的URL
func webView(_ webView: UIWebView, shouldStartLoadWith request: URLRequest, navigationType: UIWebViewNavigationType) -> Bool {
let url = request.url
let scheme = url?.scheme
if let URL = url, scheme == "anranaction" {
self.handleCustomAction(url: URL)
return false
}
return true
}
這里通過(guò)scheme,來(lái)攔截掉自定義的URL 就非常容易了,如果不同的方法使用不同的scheme,那么判斷起來(lái)就非常的麻煩,看看我們是怎么處理的
// MARK: - 處理URL然后調(diào)用方法
func handleCustomAction(url: URL) {
let host = url.host
if host == "scanClick" {
} else if host == "shareClick" {
share(url: url)
} else if host == "getLocation" {
getLocation()
} else if host == "setColor" {
ChangeColor(url: url)
} else if host == "payAction" {
payAction(url: url)
} else if host == "shake" {
sharedAction()
} else if host == "back" {
goBack()
}
}
我們?cè)贘S中調(diào)用Swift 方法的時(shí)候,也需要傳參數(shù)到Swift 中,怎么傳呢?
就像一個(gè)get 請(qǐng)求一樣,把參數(shù)放在后面:
function shareClick() {
loadURL("haleyAction://shareClick?title=標(biāo)題&content=內(nèi)容 &url=http://www.baidu.com");
}
我們?nèi)绾潍@取參數(shù),并且將參數(shù)傳回JS中,所有的參數(shù)都在URL的query中,先通過(guò)&將字符串拆分,在通過(guò)=把參數(shù)拆分成key 和實(shí)際的值
func share(url: URL) {
guard let params = url.query?.components(separatedBy: "&") else { return }
var tempDic = [String:Any]()
for paramStr in params {
let dicArray = paramStr.components(separatedBy: "=")
if dicArray.count > 1 {
guard let str = dicArray[1].removingPercentEncoding else { return }
tempDic[dicArray[0]] = str
}
}
let title = tempDic["title"]
let content = tempDic["content"]
let url = tempDic["url"]
let jsStr = "shareResult('\(title ?? "")','\(content ?? "")','\(url ?? "")')"
webView.stringByEvaluatingJavaScript(from: jsStr)
}
Swift調(diào)用JS
let jsStr = "setLocation('\("杭州市拱墅區(qū)下沙中國(guó)計(jì)量學(xué)院")')"
webView.stringByEvaluatingJavaScript(from: jsStr)
Swift中可以往HMTL的JS環(huán)境中插入全局變量、JS方法
func webViewDidFinishLoad(_ webView: UIWebView) {
print("webView加載完成然后調(diào)用")
webView.stringByEvaluatingJavaScript(from: "var arr = [3, 4, 'abc']")
}
3. WKWebView的情景
由于UIWebView比較耗內(nèi)存,性能上不太好,而蘋果在iOS 8中推出了WKWebView。
同樣的用WKWebView也可以攔截URL,做JS 與Native交互
安然 | 打開(kāi)百度網(wǎng)頁(yè)前 | 打開(kāi)百度網(wǎng)頁(yè)后 |
---|---|---|
UIWebView | 內(nèi)存47M | 內(nèi)存75.6M,最高峰83M |
WKWebView | 內(nèi)存47M | 內(nèi)存51M |
盡管WKWebView有很多的優(yōu)點(diǎn)但是也有很多的缺點(diǎn),比如他的儲(chǔ)存模式,是封閉的,我們要訪問(wèn)也是不容易的,這個(gè)問(wèn)題在以后我專門的學(xué)習(xí)一下,這篇就不在解釋了
WKWebView 與 UIWebView 攔截URL 的處理方式基本一樣。除了代理方法和WKWebView的使用不太一樣,關(guān)于WKWebView更詳盡的講解和用法,還是自行搜索學(xué)習(xí),本文重點(diǎn)還是講解如何實(shí)現(xiàn)JS 與Native互相調(diào)用
首先, 創(chuàng)建WKWebView,WKWebView的創(chuàng)建有幾點(diǎn)不同:
- 初始化configuration參數(shù),當(dāng)然這個(gè)參數(shù)我們也可以不傳,直接使用默認(rèn)的設(shè)置就好
- WKWebView的代理有兩個(gè)navigationDelegate和UIDelegate。我們要攔截URL,就要通過(guò)navigationDelegate的一個(gè)代理方法來(lái)實(shí)現(xiàn)。如果在HTML中要使用alert等彈窗,就必須得實(shí)現(xiàn)UIDelegate的相應(yīng)代理方法
- 在iOS 9之前,WKWebView加載本地HTML會(huì)有一些問(wèn)題(不能加載本地HTML,或者部分CSS/本地圖片加載不了等)
lazy var webView: WKWebView = {[unowned self] in
let configuration = WKWebViewConfiguration()
configuration.userContentController = WKUserContentController()
let preferences = WKPreferences()
preferences.javaScriptCanOpenWindowsAutomatically = true
preferences.minimumFontSize = 30.0
configuration.preferences = preferences
let view = WKWebView(frame: self.view.frame, configuration: configuration)
let urlStr = Bundle.main.path(forResource: "anran.html", ofType: nil)
let fileURL = URL(fileURLWithPath: urlStr!)
view.loadFileURL(fileURL, allowingReadAccessTo: fileURL)
view.navigationDelegate = self
view.uiDelegate = self
return view
}()
然后,使用WKNavigationDelegate中的代理方法,攔截自定義的URL來(lái)實(shí)現(xiàn)JS調(diào)用OC方法
- 如果實(shí)現(xiàn)了這個(gè)代理方法,就必須得調(diào)用decisionHandler這個(gè)block,否則會(huì)導(dǎo)致app 崩潰。block參數(shù)是個(gè)枚舉類型,WKNavigationActionPolicyCancel代表取消加載,相當(dāng)于UIWebView的代理方法return false的情況;WKNavigationActionPolicyAllow代表允許加載,相當(dāng)于UIWebView的代理方法中 return true的情況
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
let url = navigationAction.request.url
let scheme = url?.scheme
if let URL = url, scheme == "anranaction" {
self.handleCustomAction(url: URL)
// 一定要實(shí)現(xiàn)否則會(huì)崩潰
decisionHandler(.cancel)
return
}
decisionHandler(.allow)
}
最后,JS 調(diào)用Native方法后,有的操作可能需要將結(jié)果返回給JS。這時(shí)候就是Native 調(diào)用JS 方法的場(chǎng)景,WKWebView 提供了一個(gè)新的方法evaluateJavaScript:completionHandler:實(shí)現(xiàn)Native調(diào)用JS 等場(chǎng)景
func getLocation() {
let jsStr = "setLocation('\("杭州市拱墅區(qū)下沙中國(guó)計(jì)量學(xué)院")')"
webView.evaluateJavaScript(jsStr) { (result, error) in
print("\(result)")
}
}
這個(gè)方法evaluateJavaScript(<#T##javaScriptString: String##String#>, completionHandler: <#T##((Any?, Error?) -> Void)?##((Any?, Error?) -> Void)?##(Any?, Error?) -> Void#>)
沒(méi)有返回值,JS 執(zhí)行成功還是失敗會(huì)在completionHandler 中返回。所以使用這個(gè)API 就可以避免執(zhí)行耗時(shí)的JS,或者alert 導(dǎo)致界面卡住的問(wèn)題
WKWebView中使用彈窗
在上面提到,如果在WKWebView中使用alert、confirm 等彈窗,就得實(shí)現(xiàn)WKWebView的WKUIDelegate中相應(yīng)的代理方法。
如果,我在JS中要顯示alert 彈窗,就必須實(shí)現(xiàn)如下代理方法,否則alert 并不會(huì)彈出
func webView(_ webView: WKWebView, runJavaScriptAlertPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping () -> Void) {
let alert = UIAlertController(title: "提醒", message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "知道", style: .cancel, handler: { (action) in
completionHandler()
}))
self.present(alert, animated: true, completion: nil)
}
其中completionHandler
這個(gè)block 必須調(diào)用