1. 程式人生 > >iOS下Cordova與OC互相呼叫

iOS下Cordova與OC互相呼叫

2.設定網頁控制器,新增網頁

首先將 ViewController 的父類改為 CDVViewController。如下圖所示:


這裡分兩種情況,載入本地HTML 和遠端HTML 地址。
** 載入本地HTML **
載入本地HTML,為了方便起見,首先新建一個叫www的資料夾,然後在資料夾裡放入要載入的HTML和cordova.js
這裡把www新增進工程時,需要注意勾選的是create foler references,建立的是藍色資料夾。

最終的目錄結構如下:

上面為什麼說是方便起見呢?
先說答案,因為CDVViewController有兩個屬性 wwwFolderName

startPagewwwFolderName 的預設值為wwwstartPage 的預設值為 index.html

CDVViewControllerviewDidLoad方法中,呼叫了與網頁相關的三個方法:
- loadSetting- createGapView- appUrl
先看- loadSetting,這裡會對 wwwFolderNamestartPage 設定預設值,程式碼如下:

- (void)loadSettings
{
    CDVConfigParser* delegate = [[CDVConfigParser alloc] init];

    [self parseSettingsWithParser:delegate];

    // Get the plugin dictionary, whitelist and settings from the delegate.
    self.pluginsMap = delegate.pluginsDict;
    self.startupPluginNames = delegate.startupPluginNames;
    self.settings = delegate.settings;

    // And the start folder/page.
    if(self.wwwFolderName == nil){
        self.wwwFolderName = @"www";
    }
    if(delegate.startPage && self.startPage == nil){
        self.startPage = delegate.startPage;
    }
    if (self.startPage == nil) {
        self.startPage = @"index.html";
    }

    // Initialize the plugin objects dict.
    self.pluginObjects = [[NSMutableDictionary alloc] initWithCapacity:20];
}

要看- createGapView,是因為這個方法內部先呼叫了一次 - appUrl,所以關鍵還是- appUrl。原始碼如下:

- (NSURL*)appUrl
{
    NSURL* appURL = nil;

    if ([self.startPage rangeOfString:@"://"].location != NSNotFound) {
        appURL = [NSURL URLWithString:self.startPage];
    } else if ([self.wwwFolderName rangeOfString:@"://"].location != NSNotFound) {
        appURL = [NSURL URLWithString:[NSString stringWithFormat:@"%@/%@", self.wwwFolderName, self.startPage]];
    } else if([self.wwwFolderName hasSuffix:@".bundle"]){
        // www folder is actually a bundle
        NSBundle* bundle = [NSBundle bundleWithPath:self.wwwFolderName];
        appURL = [bundle URLForResource:self.startPage withExtension:nil];
    } else if([self.wwwFolderName hasSuffix:@".framework"]){
        // www folder is actually a framework
        NSBundle* bundle = [NSBundle bundleWithPath:self.wwwFolderName];
        appURL = [bundle URLForResource:self.startPage withExtension:nil];
    } else {
        // CB-3005 strip parameters from start page to check if page exists in resources
        NSURL* startURL = [NSURL URLWithString:self.startPage];
        NSString* startFilePath = [self.commandDelegate pathForResource:[startURL path]];

        if (startFilePath == nil) {
            appURL = nil;
        } else {
            appURL = [NSURL fileURLWithPath:startFilePath];
            // CB-3005 Add on the query params or fragment.
            NSString* startPageNoParentDirs = self.startPage;
            NSRange r = [startPageNoParentDirs rangeOfCharacterFromSet:[NSCharacterSet characterSetWithCharactersInString:@"?#"] options:0];
            if (r.location != NSNotFound) {
                NSString* queryAndOrFragment = [self.startPage substringFromIndex:r.location];
                appURL = [NSURL URLWithString:queryAndOrFragment relativeToURL:appURL];
            }
        }
    }

    return appURL;
}

此時執行效果圖:

** 載入遠端HTML **
專案裡一般都是這種情況,介面返回H5地址,然後用網頁載入H5地址。
只需要設定下 self.startPage就好了。

這裡有幾個需要注意的地方:

  1. self.startPage的賦值,必須在[super viewDidLoad]之前,否則self.startPage 會被預設賦值為index.html。
  2. 需要在config.xml中修改一下配置,否則載入遠端H5時,會自動開啟瀏覽器載入。
    需要新增的配置是:
<allow-navigation href="https://*/*" />
<allow-navigation href="http://*/*"  />
  1. 遠端H5中也要引用cordova.js檔案。
  2. info.plist 中新增 App Transport Security Setting的設定。

執行效果圖:

3.建立外掛,配置外掛

在外掛中實現JS要呼叫的原生方法,外掛要繼承自CDVPlugin,示例程式碼如下:

#import "CDV.h"

@interface HaleyPlugin : CDVPlugin

- (void)scan:(CDVInvokedUrlCommand *)command;

- (void)location:(CDVInvokedUrlCommand *)command;

- (void)pay:(CDVInvokedUrlCommand *)command;

- (void)share:(CDVInvokedUrlCommand *)command;

- (void)changeColor:(CDVInvokedUrlCommand *)command;

- (void)shake:(CDVInvokedUrlCommand *)command;

- (void)playSound:(CDVInvokedUrlCommand *)command;

@end

配置外掛,是在config.xml的widget中新增自己建立的外掛。
如下圖所示:

關於外掛中方法的實現有幾個注意點:
1、如果你發現類似如下的警告:

THREAD WARNING: ['scan'] took '290.006104' ms. Plugin should use a background thread.

那麼直需要將實現改為如下方式即可:

[self.commandDelegate runInBackground:^{
      // 這裡是實現
}];

示例程式碼:

- (void)scan:(CDVInvokedUrlCommand *)command
{
    [self.commandDelegate runInBackground:^{
        dispatch_async(dispatch_get_main_queue(), ^{
            UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"原生彈窗" message:nil delegate:nil cancelButtonTitle:@"知道了" otherButtonTitles:nil, nil];
            [alertView show];
        });
    }];
}

2、如何獲取JS 傳過來的引數呢?
CDVInvokedUrlCommand 引數,其實有四個屬性,分別是argumentscallbackIdclassNamemethodName。其中arguments,就是引數陣列。
看一個獲取引數的示例程式碼:

- (void)share:(CDVInvokedUrlCommand *)command
{
    NSUInteger code = 1;
    NSString *tip = @"分享成功";
    NSArray *arguments = command.arguments;
    if (arguments.count < 3) {;
        code = 2;
        tip = @"引數錯誤";
        NSString *jsStr = [NSString stringWithFormat:@"shareResult('%@')",tip];
        [self.commandDelegate evalJs:jsStr];
        return;
    }
    
    NSLog(@"從H5獲取的分享引數:%@",arguments);
    NSString *title = arguments[0];
    NSString *content = arguments[1];
    NSString *url = arguments[2];
    
    // 這裡是分享的相關程式碼......
    
    // 將分享結果返回給js
    NSString *jsStr = [NSString stringWithFormat:@"shareResult('%@','%@','%@')",title,content,url];
    [self.commandDelegate evalJs:jsStr];
}

3、如何將Native的結果回撥給JS ?
這裡有兩種方式:第一種是直接執行JS,呼叫UIWebView 的執行js 方法。示例程式碼如下:

 // 將分享結果返回給js
    NSString *jsStr = [NSString stringWithFormat:@"shareResult('%@','%@','%@')",title,content,url];
    [self.commandDelegate evalJs:jsStr];

第二種是,使用Cordova 封裝好的物件CDVPluginResult和API。
使用這種方式時,在JS 呼叫原生功能時,必須設定執行成功的回撥和執行失敗的回撥。即設定cordova.exec(successCallback, failCallback, service, action, actionArgs)的第一個引數和第二個引數。像這樣:

function locationClick() { 
    cordova.exec(setLocation,locationError,"HaleyPlugin","location",[]);
}

然後,Native 呼叫JS 的示例程式碼:

- (void)location:(CDVInvokedUrlCommand *)command
{
    // 獲取定位資訊......
    
    // 下一行程式碼以後可以刪除
//    NSString *locationStr = @"廣東省深圳市南山區學府路XXXX號";
    NSString *locationStr = @"錯誤資訊";
    
//    NSString *jsStr = [NSString stringWithFormat:@"setLocation('%@')",locationStr];
//    [self.commandDelegate evalJs:jsStr];
    
    [self.commandDelegate runInBackground:^{
        CDVPluginResult *result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:locationStr];
        [self.commandDelegate sendPluginResult:result callbackId:command.callbackId];
    }];
}

4.JS 呼叫Native 功能

終於到重點了,JS想要呼叫原生程式碼,如何操作呢?我用本地HTML 來演示。
首先,HTML中需要載入 cordova.js,需要注意該js 檔案的路徑,因為我的cordova.js與HTML放在同一個資料夾,所以src 是這樣寫:

<script type="text/javascript" src="cordova.js"></script>

然後,在HTML中建立幾個按鈕,以及實現按鈕的點選事件,示例程式碼如下:

<input type="button" value="掃一掃" onclick="scanClick()" />
        <input type="button" value="獲取定位" onclick="locationClick()" />
        <input type="button" value="修改背景色" onclick="colorClick()" />
        <input type="button" value="分享" onclick="shareClick()" />
        <input type="button" value="支付" onclick="payClick()" />
        <input type="button" value="搖一搖" onclick="shake()" />
        <input type="button" value="播放聲音" onclick="playSound()" />

點選事件對應的關鍵的JS程式碼示例:

function scanClick() {
    cordova.exec(null,null,"HaleyPlugin","scan",[]);
}

function shareClick() {
    cordova.exec(null,null,"HaleyPlugin","share",['測試分享的標題','測試分享的內容','http://m.rblcmall.com/share/openShare.htm?share_uuid=shdfxdfdsfsdfs&share_url=http://m.rblcmall.com/store_index_32787.htm&imagePath=http://c.hiphotos.baidu.com/image/pic/item/f3d3572c11dfa9ec78e256df60d0f703908fc12e.jpg']);
}

function locationClick() {
    cordova.exec(setLocation,locationError,"HaleyPlugin","location",[]);
}

function locationError(error) {
    asyncAlert(error);
    document.getElementById("returnValue").value = error;
}

function setLocation(location) {
    asyncAlert(location);
    document.getElementById("returnValue").value = location;
}

JS 要呼叫原生,執行的是:

// successCallback : 成功的回撥方法
// failCallback : 失敗的回撥方法
// server : 所要請求的服務名字,就是外掛類的名字
// action : 所要請求的服務具體操作,其實就是Native 的方法名,字串。
// actionArgs : 請求操作所帶的引數,這是個陣列。
cordova.exec(successCallback, failCallback, service, action, actionArgs);

cordova,是cordova.js裡定義的一個 var結構體,裡面有一些方法以及其他變數,關於exec ,可以看 iOSExec這個js 方法。
大致思想就是,在JS中定義一個數組和一個字典(鍵值對)。
陣列中存放的就是:

callbackId與服務、操作、引數的對應關係轉成json 存到上面全域性陣列中。
 var command = [callbackId, service, action, actionArgs];

    // Stringify and queue the command. We stringify to command now to
    // effectively clone the command arguments in case they are mutated before
    // the command is executed.
 commandQueue.push(JSON.stringify(command));

而字典裡存的是回撥,當然回撥也是與callbackId對應的,這裡的callbackId與上面的callbackId是同一個:

callbackId = service + cordova.callbackId++;
cordova.callbacks[callbackId] =
            {success:successCallback, fail:failCallback};

** iOSExec 裡又是如何呼叫到原生方法的呢?**
依然是做一個假的URL 請求,然後在UIWebView的代理方法中攔截請求。
JS 方法 iOSExec中會呼叫 另一個JS方法 pokeNative,而這個pokeNative,看到他的程式碼實現就會發現與UIWebView 開啟一個URL 的操作是一樣的:

function pokeNative() {
    // CB-5488 - Don't attempt to create iframe before document.body is available.
    if (!document.body) {
        setTimeout(pokeNative);
        return;
    }
    
    // Check if they've removed it from the DOM, and put it back if so.
    if (execIframe && execIframe.contentWindow) {
        execIframe.contentWindow.location = 'gap://ready';
    } else {
        execIframe = document.createElement('iframe');
        execIframe.style.display = 'none';
        execIframe.src = 'gap://ready';
        document.body.appendChild(execIframe);
    }
    failSafeTimerId = setTimeout(function() {
        if (commandQueue.length) {
            // CB-10106 - flush the queue on bridge change
            if (!handleBridgeChange()) {
                pokeNative();
             }
        }
    }, 50); // Making this > 0 improves performance (marginally) in the normal case (where it doesn't fire).
}

看到這裡,我們只需要去搜索一下攔截URL 的代理方法,然後驗證我們的想法介面。
我搜索webView:shouldStartLoadWIthRequest:navigationType 方法,然後打上斷點,看如下的堆疊呼叫:

關鍵程式碼是這裡,判斷url 的scheme 是否等於 gap

    if ([[url scheme] isEqualToString:@"gap"]) {
        [vc.commandQueue fetchCommandsFromJs];
        // The delegate is called asynchronously in this case, so we don't have to use
        // flushCommandQueueWithDelayedJs (setTimeout(0)) as we do with hash changes.
        [vc.commandQueue executePending];
        return NO;
    }

fetchCommandsFromJs 是呼叫js 中的nativeFetchMessages(),獲取commandQueue裡的json 字串;
executePending中將json 字串轉換為CDVInvokedUrlCommand物件,以及利用runtime,將js 裡的服務和 方法,轉換物件,然後呼叫objc_msgSend 直接呼叫執行,這樣就進入了外掛的對應的方法中了。

這一套思想與WebViewJavascriptBridge的思想很相似。

5. Native 呼叫 JS 方法

這個非常簡單,如果是在控制器中,那麼只需要像如下這樣既可:

- (void)testClick
{
    // 方式一:
    NSString *jsStr = @"asyncAlert('哈哈啊哈')";
    [self.commandDelegate evalJs:jsStr];
    
}

這裡的evalJs內部呼叫的其實是 UIWebViewstringByEvaluatingJavaScriptFromString 方法。

6.如果你在使用Xcode 8時,覺得控制檯裡大量的列印很礙眼,可以這樣設定來去掉。

首先:

然後,新增一個環境變數:

好了,到這裡關於Cordova 的講解就結束了。

在前面的文章中介紹的瞭如何使用Cordova進行跨平臺應用的開發,使用Cordova的話基本上就不需要在寫系統原生程式碼了,只要通過編寫html頁面和js方法即可。

但在有些特殊情況下,還是是需要html頁面能和系統原生程式碼(ios native code)進行互動。下面介紹如何實現 JS 與 Swift 程式碼間的相互通訊。

假設我們已經建立了一個名叫 HelloWorld 的Cordova工程專案(不太清楚如何使用Cordova的可以參考我前面寫的幾篇文章:使用Cordova開發iOS應用實戰1(配置、開發第一個應用)

原文:Cordova - 與iOS原生程式碼互動1(通過JS呼叫Swift方法)


1,樣例說明
(1)雖然使用Cordova建立的工程是一個 OC 工程,但由於蘋果可以很方便的支援混合程式設計,所以我們用 Swift 來實現與 JS 的互動也是可以的。
(2)這裡我們使用Swift來做個口令驗證的功能,由於只是演示,所以程式碼很簡單。Swift這邊接收傳輸過來的口令字串,判斷正確與否並反饋回頁面。如果驗證失敗還會返回具體的失敗原因資訊。

2,樣例效果圖

      原文:Cordova - 與iOS原生程式碼互動1(通過JS呼叫Swift方法)      原文:Cordova - 與iOS原生程式碼互動1(通過JS呼叫Swift方法)

      原文:Cordova - 與iOS原生程式碼互動1(通過JS呼叫Swift方法)      原文:Cordova - 與iOS原生程式碼互動1(通過JS呼叫Swift方法)

3,實現步驟

(1)我們在 Plugins 資料夾中新建一個Swift檔案(HanggeSwiftPlugin.swift)。建立的時候系統會提示是否同時建立橋接標頭檔案,這裡選擇確定。

原文:Cordova - 與iOS原生程式碼互動1(通過JS呼叫Swift方法)

(2)在新生成的橋接標頭檔案 HelloWorld-Bridging-Header.h 中把 Cordova/CDV.h 給 import 進來

1

#import <Cordova/CDV.h>


(3)新建的 HanggeSwiftPlugin.swift 中新增如下程式碼 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

import Foundation

@objc(HWPHanggeSwiftPlugin) class HanggeSwiftPlugin : CDVPlugin {

//驗證口令方法

func verifyPassword(command:CDVInvokedUrlCommand)

{

//返回結果

var pluginResult:CDVPluginResult?

//獲取引數

let password = command.arguments[0] as? String

//開始驗證

if password == nil || password == "" {

pluginResult = CDVPluginResult(status: CDVCommandStatus_ERROR,

messageAsString: "口令不能為空")

}else if password != "hangge" {

pluginResult = CDVPluginResult(status: CDVCommandStatus_ERROR,

messageAsString: "口令不正確")

}else{

pluginResult = CDVPluginResult(status:CDVCommandStatus_OK)

}

//傳送結果

self.commandDelegate.sendPluginResult(pluginResult, callbackId: command.callbackId)

}

}

(4)在 config.xml 中新增如下配置,將我們建立的功能類給配置上

1

2

3

<feature name="HanggeSwiftPlugin">

<param name="ios-package" value="HWPHanggeSwiftPlugin" />

</feature>

(5)首頁 index.html 程式碼如下,修改執行後即可看到效果。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

<!DOCTYPE html>

<html>

<head>

<title>口令驗證</title>

<meta http-equiv="Content-type" content="text/html; charset=utf-8">

<script type="text/javascript" charset="utf-8" src="cordova.js"></script>

<script type="text/javascript" charset="utf-8">

//開始驗證

function verify() {

//獲取輸入的口令

var password = document.getElementById("pwd").value;

//呼叫自定義的驗證外掛

Cordova.exec(successFunction, failFunction, "HanggeSwiftPlugin",

"verifyPassword", [password]);

}

//驗證成功

function successFunction(){

alert("口令驗證成功!");

}

//驗證失敗

function failFunction(message){

alert("驗證失敗:"+message);

}

</script>

<style>

* {

font-size:1em;

}

</style>

</head>

<body style="padding-top:50px;">

<input type="text" id="pwd" >

<button onclick="verify();">驗證</button>

</body>

</html>