Swift和Javascript的神奇魔法

Swift和Javascript的神奇魔法

記錄Swift和Javascript如何進行交互javascript

前言

今天在網上看到了一篇介紹Swift和Javascript交互的文章,感受做者寫的很好,所以把做者文章中的主要知識點進行一個總結。html

對於我我的而言,在項目中使用Javascript的緣由有兩個:java

  • 某些任務,極可能已經有現成的Javascript庫存在了,使用起來比原生實現更簡單
  • 在架構上的考慮

能夠再這裏下載演示demoios

demo中咱們主要演示了3大塊Swift和Javascript交互的神奇魔法:git

  • 在Swift中獲取和使用Javascript的屬性和函數,處理Javascript的異常,在Javascript中獲取和使用Swift的屬性和函數
  • 使用Javascript第三方庫Snowdown把Markdown文本轉換成HTML文本
  • 使用Javascript解析複雜的數據,而後用Swift展現

效果圖:github

Model,Initial OS,Latest OS,Image URL
iPhone (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.png
iPhone 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.png
iPhone 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.png
iPhone 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.png
iPhone 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.png
iPhone 5,iOS 6.0,iOS 10.2.1,https://upload.wikimedia.org/wikipedia/commons/thumb/f/fa/IPhone_5.png/99px-IPhone_5.png
iPhone 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.png
iPhone 5S,iOS 7.0,iOS 10.2.1,https://upload.wikimedia.org/wikipedia/commons/thumb/5/5e/IPhone_5s.png/88px-IPhone_5s.png
iPhone 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.png
iPhone 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.png
iPhone 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.png
iPhone 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.png
iPhone 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.png
iPhone 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.png
iPhone 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

Swift,Javascript的基本交互

JavaScriptCore 中最主要的角色就是 JSContext 類。一個 JSContext 對象是位於 JavaScript 環境和本地 Javascript 腳本之間的橋樑。數組

所以須要初始化一個JSContext對象:markdown

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 UIKit
import 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的函數有返回值。

總結

在ios7以前咱們只能經過UIWebview才能調用Javascript代碼,如今,咱們經過JavascriptCore能夠自由使用Javascript。但在使用的時候要特別注意內存管理問題,大概須要注意一下兩點:

  • 不要在block裏面直接使用context,或者使用外部的JSValue對象。
  • 對象不要用屬性直接保存JSValue對象,由於這樣太容易循環引用了。

可使用JSManagedValue去解決這個問題。

參考連接

JavaScriptCore官方文檔

Using JavaScript in Swift Projects: Building a Markdown to HTML Editor

如何在Swift項目中使用 Javascript編寫一個將Markdown轉爲HTML的編輯器

JavaScriptCore 使用

相關文章
相關標籤/搜索