前言
當前混合開發模式迎來了前所未有的發展,跨平臺開發、熱更新等優點決定了這種模式的重要地位。雖然前端界面在交互、動效等多方面距離原生應用還有差距,但毫無疑問混合開發只會被越來越多的公司接受。在iOS中,混合開發模式被分為兩個時代,分別是iOS7之前的坑爹時代與之后的黃金時代,其分割代表為JavaScriptCore
框架
坑爹時代
作為完美避開iOS7之前版本的幸運兒,我只能從某位前輩的口中得知那悲慘的歲月。作為那個年代唯一能與前端界面交互的手段就是UIWebView
,先不說它自身的內存泄露缺陷,下面是一段前輩寫過的代碼:
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
{
NSString * address = request.URL.absoluteString;
for (NSString * black in _blackList) {
if ([address containsString: black]) {
return NO;
}
}
for (NSString * event in _eventList) {
if ([address containsString: event]) {
SEL callback = NSSelectorFromString(_callbacks[event]);
[self performSelector: callback];
return [event containsString: @"shouldOpen=1"];
}
}
return YES;
}
在那個年代,前輩的小伙伴們把前端事件的觸發條件設置為鏈接跳轉,然后通過鏈接中的關鍵字符來判斷處理操作。為此,需要定義好些個數據集合來存儲這些關鍵字符的處理操作。如果遇到應用和前端交換交互數據的時候,那一長串的參數字符全部拼接在請求地址里,想想也是醉了。另外的交互方法就是通過stringByEvaluatingJavaScriptFromString
方法來執行js
代碼。
JavaScriptCore
JavaScriptCore
是一套用來對JS
代碼進行解析和提供執行環境的開源框架,極大的簡化了我們的交互過程。下面從項目和JS
代碼相互調用的兩個不同操作介紹其中相對應的方法
項目調用JS代碼
JSContext
一個JSContext
對象是JavaScript
運行的全局環境對象,它提供了代碼運行和注冊方法接口的服務。下面的代碼就創建了一個JSContext
對象,并且定義了一部分的JS
代碼加入到執行環境中
let context = JSContext()
context.evaluateScript(" var age = 22 ")
context.evaluateScript(" var name = 'SindriLin' ")
context.evaluateScript(" var birth = 1993-01-01 ")
context.evaluateScript(" var createPerson =
function(age, name, birth)
{
return {'age': age, 'name': name, 'birth': birth}
} ")
context.evaluateScript(" var codeDescription = 'The code create three value and a function to create a dictionary stored person information' ")
此外,在JS
代碼執行過程中,可能會出現語法錯誤等多種錯誤,通過下面的代碼可以對這些錯誤進行處理
context?.exceptionHandler = { context, exception in
print("Java Script Run Error: (exception)")
}-
JSValue
JSValue
是所有JSContext
操作后返回的值,包裝了幾乎所有的數據類型,包括錯誤和IMP
指針等。在JSValue
類結構中存在多個toXXXX
命名的方法轉換成iOS
數據類型以及call
方法來調用方法。下面的代碼從JSContext
環境中獲取已存在的部分變量,并且執行創建一個存儲person
信息的字典
let age = context?.objectForKeyedSubscript("age")
let name = context?.objectForKeyedSubscript("name")
let birth = context?.objectForKeyedSubscript("birth")
let createFunction = context?.objectForKeyedSubscript("createPerson")
let codeDescription = context?.objectForKeyedSubscript("codeDescription")
let person = createFunction.call(withArguments: [age.toInt32(), name.toString(), birth.toString()])let personInfo = "name: \(person["name"]) age: \(person["age"] and birth: \(person["birth"])" print("The javaScript code description: \(codeDescription.toString())") print("The created person \(personInfo) ")
通過上面的例子,我們可以看到,只要了解到JS
代碼中我們需要調用的方法信息,通過JSContext + JSValue
的方式我們就能輕松的在項目中調用前端界面的方法,而不再需要拼接長串參數字符通過鏈接地址傳遞給前端界面
JS調用項目代碼
JavaScript
訪問我們代碼中的對象以及方法有兩種方式:Blocks
和JSExport
。
-
Blocks
自定義的block
代碼可以通過JSContext
轉換成JS
代碼中的函數指針調用,這里存在一個坑就是Swift
中的閉包無法完成這樣的類型轉換,因此這種方式的操作流程在Swift
中是這樣的:Closure
->block
->function pointer
。在閉包轉成block
的這一過程中,需要使用一個重要的關鍵符@convention
let stringConvert: @convention(block) (String)->String = {
let pinyin = NSMutableString(string: $0) as CFMutableString
CFStringTransform(pinyin, nil, kCFStringTransformToLatin, false)
CFStringTransform(pinyin, nil, kCFStringTransformStripCombiningMarks, false)
return pinyin as String
}let convertObjc = unsafeBitCast(stringConvert, to: AnyObject.self) context?.setObject(convertObjc, forKeyedSubscript: "convertFunc") let convertFunc = context?.objectForKeyedSubscript("convertFunc") print("林欣達的拼音是\(convertFunc.call(withArguments: ["林欣達"]).toString())")
這時候,只要前端在
JS
的按鈕點擊代碼中調用convertFunc()
這句代碼就會執行這個closure
中的代碼。使用這種方式要注意由于閉包的捕獲特性,有可能會導致你的JSContext
對象被引用而無法被釋放,使用JSContext.current()
獲取當前上下文來解決引用問題 JSExport
在JS
中調用iOS
方法的時候,通過調用JSExport
的派生協議方法來實現。所有派生協議的方法會自動提供給JavaScript
代碼使用,這個在下面的demo中可以看到
實戰
在本文demo中我寫了一段JS
代碼,下面放出這段代碼以及運行效果。其中要注意的是按鈕的onclik
表示按鈕點擊的響應事件:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
<div style="margin-top: 20px">
<h2 align="center" style="color:#ff0000">JS與iOS交互</h2>
<input type="button" value="點擊后切換控制器的背景顏色" onclick="sindrilin.call()">
</div>
<div style="color:#7BBDE5">
<br />
<br />
賬戶:
<input id="account" type="text">
<br />
密碼:
<input id="password" type="password">
</div>
<div>
<input type="button" value="登錄" onclick="login()">
</div>
<script>
var login = function()
{
account = document.getElementById("account")
password = document.getElementById("password")
var accountInfo = JSON.stringify({"account": account.value, "password": password.value});
sindrilin.login(accountInfo);
}
var alertFromIOS = function(message)
{
alert(message)
}
</script>
</body>
</html>
首先我們需要加載這個
HTML
文件,然后獲取代碼運行的全局環境對象。基本上在所有的HTML
格式文件中,獲取環境對象的keyPath
都是一樣的:
let jsPath = Bundle.main().pathForResource("interaction", ofType: "html")
webView.loadRequest(URLRequest(url: URL(fileURLWithPath: jsPath!)))
interactionContext = webView.value(forKeyPath: "documentView.webView.mainFrame.javaScriptContext") as? JSContext
interactionContext?.exceptionHandler = {
print("Interaction Error: \($1?.toString())")
}
對照HTML
代碼,最上面的按鈕點擊之后會調用一個sindrilin.call()
的方法,這個方法最終要由我們的控制器來進行處理。我們可以把這個字符串分成類似Target-Action
機制的兩部分,前者sindrilin
表示響應者,后面call()
表示響應事件。其中Target
的設置方式如下
interactionContext?.setObject(self, forKeyedSubscript: "sindrilin")
響應者已經有了,那么響應事件也要我們實現代碼,這里就需要用到JSExport
協議了。所有這種類似Target-Action
的事件觸發都會通過這個協議獲取方法實現,因此我們需要自定義響應協議以及響應事件。對于有參數的方法我們需要用@objc(name)
的方式給方法起OC
式的方法名,才能保證能被正確調用響應:
@objc protocol LXDInteractionExport: JSExport {
func call() ///響應sindrilin.call()
@objc(login:) func login(accountInfo: String) ///響應sindrilin.login(accountInfo)
}
extension ViewController: LXDInteractionExport {
func call() {
print("call from html button clicked")
view.backgroundColor = UIColor(red: CGFloat(arc4random() % 256) / 255, green: CGFloat(arc4random() % 256) / 255, blue: CGFloat(arc4random() % 256) / 255, alpha: 1)
}
func login(accountInfo: String) {
do {
if let JSON: [String: String] = try JSONSerialization.jsonObject(with: accountInfo.data(using: String.Encoding.utf8)!, options: JSONSerialization.ReadingOptions()) as? [String: String] {
print("JSON: \(JSON)")
let alert = interactionContext?.objectForKeyedSubscript("alertFromIOS")
let message = "The alert from javascript call\naccount: \(JSON["account"]) and password: \(JSON["password"])"
_ = alert?.call(withArguments: [message])
}
} catch {
print("Error: \(error)")
}
}
}
用戶在前端界面輸入賬戶和密碼信息之后點擊登錄就會調用login(accountInfo: String)
方法,將用戶名和密碼拼湊成JSON
字符串傳遞過來。在響應方法中我解析獲取對應字段的用戶信息,并且組轉成新的字符串調用JS
的彈窗函數彈出響應。demo下載
關注iOS開發文集收看更多文章
轉載請注明原文作者和地址