前言
今天在網上看到了一篇介紹Swift和Javascript交互的文章,感覺作者寫的很好,因此把作者文章中的主要知識點進行一個總結。
對於我個人而言,在項目中使用Javascript的原因有兩個:
某些任務,很可能已經有現成的Javascript庫存在了,使用起來比原生實現更簡單
在架構上的考慮
可以再這裏下載演示demo
demo中我們主要演示了3大塊Swift和Javascript交互的神奇魔法:
在Swift中獲取和使用Javascript的屬性和函數,處理Javascript的異常,在Javascript中獲取和使用Swift的屬性和函數
使用Javascript第三方庫Snowdown把Markdown文本轉換成HTML文本
使用Javascript解析複雜的數據,然後用Swift展示
效果圖:
Model,Initial OS,Latest OS,Image URLiPhone (1st Generation),iPhone OS 1.0,iPhone OS 3.1.3,https://upload.wikimedia.org/wikipedia/commons/thumb/0/02/IPhone_2G_PSD_Mock.png/81px-IPhone_2G_PSD_Mock.pngiPhone 3G,iPhone OS 2.0,iOS 4.2.1,https://upload.wikimedia.org/wikipedia/commons/thumb/c/c6/IPhone_PSD_White_3G.png/81px-IPhone_PSD_White_3G.pngiPhone 3GS,iPhone OS 3.0,iOS 6.1.6,https://upload.wikimedia.org/wikipedia/commons/thumb/c/c6/IPhone_PSD_White_3G.png/81px-IPhone_PSD_White_3G.pngiPhone 4,iOS 4.0,iOS 7.1.2,https://upload.wikimedia.org/wikipedia/commons/thumb/5/59/IPhone_4_Mock_No_Shadow_PSD.png/81px-IPhone_4_Mock_No_Shadow_PSD.pngiPhone 4S,iOS 5.0,iOS 9.3.5,https://upload.wikimedia.org/wikipedia/commons/thumb/d/d2/IPhone_4S_No_shadow.png/99px-IPhone_4S_No_shadow.pngiPhone 5,iOS 6.0,iOS 10.2.1,https://upload.wikimedia.org/wikipedia/commons/thumb/f/fa/IPhone_5.png/99px-IPhone_5.pngiPhone 5C,iOS 7.0,iOS 10.2.1,https://upload.wikimedia.org/wikipedia/commons/thumb/9/97/IPhone_5C_%28blue%29.svg/88px-IPhone_5C_%28blue%29.svg.pngiPhone 5S,iOS 7.0,iOS 10.2.1,https://upload.wikimedia.org/wikipedia/commons/thumb/5/5e/IPhone_5s.png/88px-IPhone_5s.pngiPhone 6,iOS 8.0,iOS 10.2.1,https://upload.wikimedia.org/wikipedia/commons/thumb/0/01/IPhone6_silver_frontface.png/100px-IPhone6_silver_frontface.pngiPhone 6 Plus,iOS 8.0,iOS 10.2.1,https://upload.wikimedia.org/wikipedia/commons/thumb/5/55/IPhone_6_Plus_Space_Gray.svg/120px-IPhone_6_Plus_Space_Gray.svg.pngiPhone 6S,iOS 9.0,iOS 10.2.1,https://upload.wikimedia.org/wikipedia/commons/thumb/a/a7/IPhone_6S_Rose_Gold.png/105px-IPhone_6S_Rose_Gold.pngiPhone 6S Plus,iOS 9.0,iOS 10.2.1,https://upload.wikimedia.org/wikipedia/commons/thumb/a/a7/IPhone_6S_Rose_Gold.png/125px-IPhone_6S_Rose_Gold.pngiPhone SE,iOS 9.3,iOS 10.2.1,https://upload.wikimedia.org/wikipedia/en/thumb/d/d0/IPhone_SE_%28rose_gold%29.png/95px-IPhone_SE_%28rose_gold%29.pngiPhone 7,iOS 10.0,iOS 10.2.1,https://upload.wikimedia.org/wikipedia/commons/thumb/1/18/IPhone_7_Jet_Black.svg/105px-IPhone_7_Jet_Black.svg.pngiPhone 7 Plus,iOS 10.0,iOS 10.2.1,https://upload.wikimedia.org/wikipedia/commons/thumb/6/64/IPhone_7_Plus_Jet_Black.svg/125px-IPhone_7_Plus_Jet_Black.svg.png
把上邊的數據解析後,展示爲:
Swift,Javascript的基本交互
JavaScriptCore 中最主要的角色就是 JSContext 類。一個 JSContext 對象是位於 JavaScript 環境和本地 Javascript 腳本之間的橋樑。
因此需要初始化一個JSContext對象:
var jsContext: JSContext!
我不會像原文那樣一步一步的演示功能,我只是記錄下使用JSContext的核心思想和用法。
我們看看JSContext的初始化方法:
func initializeJS() { self.jsContext = JSContext() /// Catch exception self.jsContext.exceptionHandler = { context, exception in if let ex = exception { print("JS exception: " + ex.toString()) } } let jsPath = Bundle.main.path(forResource: "jssource", ofType: "js") if let path = jsPath { do { let jsSourceContents = try String(contentsOfFile: path) jsContext.evaluateScript(jsSourceContents) } catch let ex { print(ex.localizedDescription) } } // Configurate log let consoleLogObject = unsafeBitCast(self.consoleLog, to: AnyObject.self) jsContext.setObject(consoleLogObject, forKeyedSubscript: "consoleLog" as (NSCopying & NSObjectProtocol)) jsContext.evaluateScript("consoleLog") }
上邊的代碼中做了下邊這幾件事:
使用JSContext()初始化JSContext對象
JSContext中有一個屬性exceptionHandler用來監聽Javascript的錯誤。這個屬性很有用,我們使用這個屬性來發現Javascript的錯誤
JSContext的evaluateScript方法可以把數據調入到JavaScriptCore的運行時環境中。該方法需要傳遞的參數是Javascript代碼。返回值爲Javascript代碼中的最後一個JSValue。
let consoleLogObject = unsafeBitCast(self.consoleLog, to: AnyObject.self) unsafeBitCast用作強制類型轉換,使用的時候需要明確的知道要轉換的類型
open func setObject(_ object: Any!, forKeyedSubscript key: (NSCopying & NSObjectProtocol)!)通過這種方式爲Javascript添加屬性或者函數
那麼,接下來,我們看一段Swift中獲取Javascript屬性的代碼:
func helloWorld() { if let valiableHW = jsContext.objectForKeyedSubscript("helloWorld") { print(valiableHW.toString()) } }
由上邊的代碼可以看出,通過函數open func objectForKeyedSubscript(_ key: Any!) -> JSValue!可以獲取JSValue,然後使用toString()獲取字符串。
除了獲取屬性外,下邊的代碼演示瞭如何使用Javascript中的函數:
func jsDemo1() { let firstName = "zhang" let lastName = "san" if let funcFullName = jsContext.objectForKeyedSubscript("getFullName") { if let fullName = funcFullName.call(withArguments: [firstName, lastName]) { print(fullName) } } }
通過函數open func objectForKeyedSubscript(_ key: Any!) -> JSValue!可以獲取JSValue,然後調用call函數,並傳遞參數過去就實現了這個功能。
我們在看看js代碼中是如何使用Swift屬性和函數的:
function generateLuckyNumbers() { consoleLog("打印東東啊"); var luckyNumbers = []; while (luckyNumbers.length != 6) { var randomNumber = Math.floor((Math.random() * 50) + 1); if (!luckyNumbers.includes(randomNumber)) { luckyNumbers.push(randomNumber); } } handleLuckyNumbers(luckyNumbers); }
上邊代碼中的handleLuckyNumbers函數就是Swift中的函數,大家可以去demo中查看。
Markdown文本轉換成HTML文本
這個文本轉換最核心的內容就是解析Markdown的語法,然後輸出HTML文本,如果我們自己手寫轉換代碼,那就太麻煩了。Javascript已經有一個很強大的第三方庫Snowdown。
在JSContext的初始化方法中添加下邊的代碼:
// Fetch and evaluate the Snowdown script.let snowdownScript = try String(contentsOf: URL(string: "https://cdn.rawgit.com/showdownjs/showdown/1.6.3/dist/showdown.min.js")!)self.jsContext.evaluateScript(snowdownScript)
上邊的代碼中把轉換腳本調入Javascript運行時,然後我們再通過下邊的代碼調用Javascript的代碼:
func convertMarkdownToHTML() { if let funcConvertMarkdownToHTML = jsContext.objectForKeyedSubscript("convertMarkdownToHTML") { funcConvertMarkdownToHTML.call(withArguments: [self.tvEditor.text]) } }
Javascript的代碼如下:
function convertMarkdownToHTML(source) { var converter = new showdown.Converter(); var htmlResult = converter.makeHtml(source); consoleLog(htmlResult); }
核心思想就是接受Javascript轉換後的結果。
自定義類和JavaScript
前面,我們學習瞭如何暴露 Swift 程序代碼給 JS,但 JavaScriptCore 的功能並不僅限於此。它還提供一種暴露自定義類的機制,並直接在 JS 中使用這些類的屬性和函式。這就是 JSExport,它是一個協議,通過它你能夠以更強大的方式來溝通 Swift 和 JS。
我們看看自定義類的代碼:
import UIKitimport JavaScriptCore@objc protocol DeviceInfoJSExport: JSExport { var model: String! { get set} var initialOS: String! { get set} var latestOS: String! { get set} var imageURL: String! { get set} static func initializeDevice(withModel: String) -> DeviceInfo}class DeviceInfo: NSObject, DeviceInfoJSExport { var model: String! var initialOS: String! var latestOS: String! var imageURL: String! init(withModel model: String) { super.init() self.model = model } class func initializeDevice(withModel: String) -> DeviceInfo { return DeviceInfo(withModel: withModel) } func concatOS() -> String { if let initial = initialOS { if let latest = latestOS { return initial + "-" + latest } } return "" } }
如果我們實現了JSExport協議,那麼 JavaScript 運行時就能捕獲該協議中的內容。對於這種設計,可以讓我們很靈活的使用它的功能。
再看看Javascript中關於這一段的核心代碼:
function parseiPhoneList(originalData) { var results = Papa.parse(originalData, { header: true }); if (results.data) { var deviceData = []; for (var i=0; i < results.data.length; i++) { var model = results.data[i]["Model"]; var deviceInfo = DeviceInfo.initializeDeviceWithModel(model); deviceInfo.initialOS = results.data[i]["Initial OS"]; deviceInfo.latestOS = results.data[i]["Latest OS"]; deviceInfo.imageURL = results.data[i]["Image URL"]; deviceData.push(deviceInfo); } return deviceData; } return null; }
上邊的代碼,調用了第三方解析庫的函數,把數據解析出來後,生成deviceInfo數組,然後我們在Swift中就獲取到了解析好的數據:
func parseDeviceData() { if let path = Bundle.main.path(forResource: "iPhone_List", ofType: "csv") { do { let contents = try String(contentsOfFile: path) if let functionParseiPhoneList = self.jsContext.objectForKeyedSubscript("parseiPhoneList") { if let parsedDeviceData = functionParseiPhoneList.call(withArguments: [contents]).toArray() as? [DeviceInfo] { self.deviceInfo = parsedDeviceData self.tblDeviceList.reloadData() } } } catch { print(error.localizedDescription) } } }
實現這些功能的基礎就是Javascript的函數有返回值。