前言
在iOS開發過程中,一般會有遇到需要和UIWebView交互的需求,即native端和網頁端的數據交互,因為在項目中遇到過類似的開發需求,項目中用了最簡單直接的方法通過UIWebViewDelegate攔截請求的url的方式來做,這里作一個總結。
實現過程
既然要交互,所以要做到JS調用OC的代碼,而OC可以調用到JS的代碼,項目中,服務端需要創建并引入一個js文件,這個js文件定義了網頁端和OC端交互過程中需要調用的各個方法,我們可以在網頁端window中添加一個OC項目的對象ocProjectObject,ocProjectObject里再定義一個NativeBridge對象,這個對象里統一定義js調用OC代碼的call方法和需要OC方回調的方法,這樣的話當js需要調用OC的方法時可以這樣做:window.ocProjectObject.NativeBridge.call(…)
call方法為js調用OC的統一的方法,方法中包含著functionName事件名和需要傳遞給OC的參數,參數定義為json格式的字符串,另外還允許是回調函數,而在webView加載過程中,OC通過攔截到的URL判斷是否包含指定格式的鏈接,如果包含的話,則解析對應的js中包含的各參數,從而進行對應的本地的OC操作,最后把OC執行結果resultForCallback的js方法回傳給網頁端,通過下面用代碼來看看這個js應該如何實現:
定義一個NativeBridge對象
varNativeBridge = {
callbacksCount :1,
callbacks : {},
// Automatically called by native layer when a result is available
resultForCallback :functionresultForCallback(callbackId, callbackType, resultArray) {
try{
varcallback = NativeBridge.callbacks[callbackId];
if(!callback)return;
varcallbackFunc;
if(callbackType =="ok"){
callbackFunc = callback.success;
}
elseif(callbackType =="fail"){
callbackFunc = callback.failure;
}
elseif(callbackType =="cancel"){
callbackFunc = callback.cancel;
}
else{
callbackFunc = callback.complete;
}
if(callbackFunc){
callbackFunc.apply(null,resultArray);
}
}catch(e) {
alert(e)
}
},
// 用這段js代碼來調用OC的代碼
// functionName : string類型
// args : 可以是json格式的字符串或者是回調方法
call :functioncall(functionName, args) {
varhasCallback = args.success || args.failure || args.complete || args.cancel;
varcallbackId = hasCallback ? NativeBridge.callbacksCount++ :0;
if(functionName =="on") {
callbackId = args.event;
}
if(hasCallback)
NativeBridge.callbacks[callbackId] = args;
variframe = document.createElement("IFRAME");
iframe.setAttribute("src",“ocProject:"+ functionName +":"+ callbackId+":"+ encodeURIComponent(JSON.stringify(args)));
document.documentElement.appendChild(iframe);
iframe.parentNode.removeChild(iframe);
iframe =null;
}
};
創建OC項目的對象
if(!window.ocProjectObject) {
window.ocProjectObject = {};
}
window.ocProjectObject.NativeBridge =NativeBridge;
js中調用OC的一個方法可以這樣做:
ocProjectObject.NativeBridge.call("checkJsApi",args);//告訴OC需要調用checkJsApi這個方法,并傳入對應的參數
OC端代碼模塊設計
在項目中我將OC端的代碼分為三個主要的類:
JSApiManager
JSApiBase
OnFunctionApi
JSApiManager
這個類起到了JS和OC交互的管理者作用,包含事件回調api,UIWebView對象以及處理URL的方法,來看一下JSApiManager.h的設計,代碼如下:
@interfaceJSApiManager :NSObject
@property(nonatomic,strong)MCOnFunctionApi*onFunctionApi;
- (instancetype)initWithWebView:(UIWebView*)webView;
/**
*? 處理url 請求
*
*? @param request 待處理的url
*
*? @return YES 表示api 有處理,NO表示 api 不需要處理
*/
- (BOOL)handleRequest:(NSURLRequest*)request;
@end
JSApiBase
先講講這個類的作用,然后再講JSApiManager的實現可能更加思路清晰,假設現在有一個需求是:點擊網頁端的定位按鈕獲取當前用戶位置的。這時候我需要增加一個GetLocationApi的類,這個類是處理位置信息獲取和回調給js的作用,因為每個功能的api的處理方式都是一樣的,所以定義一個api基類JSApiBase,讓GetLocationApi繼承于這個基類,基類的實現大概是這樣的:
import <Foundation/Foundation.h>
typedefvoid(^JSSuccessBlock)(NSArray *args);
typedefvoid(^JSFailureBlock)(id error);
@interface JSApiBase :NSObject
/**
*? Api 名稱
*/
@property(nonatomic,strong,readonly)NSString*name;
- (void)processWithParameters:(id)params success:(JSSuccessBlock)success failure:(JSFailureBlock)failure;
@end
可以看到api基類的定義,其中name字段表示每個api對應的名稱,如獲取位置的api為getlocation,則name字段的值就是getlocation字符串,而processWithParameters是主要處理根據接收到的js傳過來的參數作本地的操作,并在處理完成之后通過定義好的成功和失敗的回調反饋給網頁端執行的結果。
所以,現在getLocationApi類要做的就是繼承于JSApiBase,然后重寫其中的name的getter方法和processWithParameters方法即可,這樣就能清晰的分離出每個功能對應一個api類,處理每個單獨的交互功能,如下是GetLocationApi的實現類:
@interface GetLocationApi()
@property(nonatomic,copy)JSSuccessBlocksuccessBlock;
@property(nonatomic,copy)JSFailureBlockfailureBlock;
@implementation GetLocationApi
- (NSString*)name
{
return@"getlocation";
}
- (void)processWithParameters:(id)params success:(JSSuccessBlock)success failure:(JSFailureBlock)failure
{
//if has params
NSString *params = [paramsobjectForKey:@“paramName"];
self.successBlock= success;
self.failureBlock= failure;
//TODO開始定位
}
- (void)locationUpdateSuccess:(LocationObj *)userLocation{
//定位成功回調
NSDictionary*result =@{@"latitude":@(userLocation.location.coordinate.latitude),
@"longitude":@(userLocation.location.coordinate.longitude)};
//通過上面的js定義的回調方法中可以看到,我們把回調結果封裝在一個數組里作為回調值,所以可以返回多個回調參數,網頁端只需要一個個取出并解析即可
if(self.successBlock) {
self.successBlock(@[result]);
self.successBlock=nil;// 設置為空,避免重復調用,因為一起請求會有多個回調。
}
}
- (void)locationUpdateFail:(NSError *)error {
//定位失敗回調,定位失敗的時候直接返回一個錯誤的字符串
NSString*errMsg = [errordescription];
if(self.failureBlock) {
self.failureBlock(errMsg);
}
}
@end
JSApiManager實現
了解了如何通過單個api來實現某個交互功能的時候,那如何調度這些個功能的正常分發呢,需要回到JSApiManager來實現,我們來看看JSApiManager這個類的實現是要如何做,先貼出JSApiManager實現類的代碼:
#import“GetLocationApi.h”
#import “CheckJsApi.h"
@interfaceJSApiManager()
@property(nonatomic,weak)UIWebView*webView;
@property(nonatomic,strong)NSMutableDictionary*apiHandlers;
@end
@implementationJSApiManager
- (instancetype)initWithWebView:(UIWebView*)webView
{
if(self= [superinit]) {
_webView= webView;
_apiHandlers= [NSMutableDictionarynew];
JSApiBase*api = [GetLocationApinew];
api = [OnFunctionApinew];
_onFunctionApi= (OnFunctionApi*)api;
[_apiHandlerssetObject:apiforKey:api.name];
// 檢查的方法必須放在最后,才能知道所有的方法
api = [[CheckJsApialloc]initWithHanlders:_apiHandlers];
[_apiHandlerssetObject:apiforKey:api.name];
}
returnself;
}
- (BOOL)handleRequest:(NSURLRequest*)request
{
NSString*requestString = [[requestURL]absoluteString];
NSLog(@"request : %@",requestString);
if([requestStringhasPrefix:@"ocProject:"]) {
NSArray*components = [requestStringcomponentsSeparatedByString:@":"];
NSString*function = (NSString*)[componentsobjectAtIndex:1];
NSString*callbackId = ((NSString*)[componentsobjectAtIndex:2]);
NSString*argsAsString = [(NSString*)[componentsobjectAtIndex:3]
stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
NSData*data = [argsAsStringdataUsingEncoding:NSUTF8StringEncoding];
NSError*error =nil;
idparams = [NSJSONSerializationJSONObjectWithData:dataoptions:0error:&error];
NSString*functionLowercase = [functionlowercaseString];
JSApiBase*api = [_apiHandlersobjectForKey:functionLowercase];
[apiprocessWithParameters:paramssuccess:^(NSArray*responseArgs) {
[selfreturnResult:callbackIdcallbackType:@"ok"args:responseArgs];
}failure:^(iderror) {
[selfreturnResult:callbackIdcallbackType:@"fail"args:@[error]];
}];
returnYES;
}
returnNO;
}
- (void)returnResult:(NSString*)callbackId callbackType:(NSString*)type args:(NSArray*)args;
{
if(!callbackId)return;
NSData *jsonData = [NSJSONSerializationdataWithJSONObject:argsoptions:0error:nil];
NSString *resultArrayString = [[NSStringalloc]initWithData:jsonDataencoding:NSUTF8StringEncoding];
// We need to perform selector with afterDelay 0 in order to avoid weird recursion stop
// when calling NativeBridge in a recursion more then 200 times :s (fails ont 201th calls!!!)
[selfperformSelector:@selector(returnResultAfterDelay:)withObject:[NSStringstringWithFormat:@"window.mc.mailchatBridge.resultForCallback('%@','%@',%@);",callbackId,type,resultArrayString]afterDelay:0];
}
-(void)returnResultAfterDelay:(NSString*)str {
// Now perform this selector with waitUntilDone:NO in order to get a huge speed boost! (about 3x faster on simulator!!!)
NSLog(@"callback string = %@",str);
[self.webViewperformSelectorOnMainThread:@selector(stringByEvaluatingJavaScriptFromString:)withObject:strwaitUntilDone:NO];
}
@end
在這個類里面,我們定義了一個webView,作用是當需要給網頁端回調結果的時候通過webView來執行回調的js方法,然后定義了一個apiHandlers字典,這個字典里存儲著所有交互功能的api對象,比如獲取位置的GetLocationApi對象,作用是在webView攔截到js的請求的時候,通過api的名稱找到對應的api處理類,然后分發對應的參數給這個類去處理相應的業務邏輯。
可能大家還注意到一個api就是OnFunctionApi這個類,它是比較特殊的一個類,就是處理js事件回調的一個api,我們規定只有需要調用js方法的時候才需要這個回調,目前還沒有具體用到,原理就是js把需要回調的事件名稱傳給OC,OC會根據這個事件在之后的某個時機去執行這個js方法。
再說說CheckJsApi這個類,這個類是供網頁端在調用OC端的代碼時事先檢查OC端是否能處理這個對應的業務,也就是js每次要調用OC代碼的時候會先來問問OC端能否處理我的這個業務,如果不能的話就不繼續執行對應的js方法,這樣也可以知道網頁端和OC端代碼是否同步。
handleRequest
重點說說這個方法,這個方法傳入一個NSURLRequest對象,這個對象就是我們在點擊網頁端的時候會在UIWebView代理方法shouldStartLoadWithRequest返回的,也就是網頁端重定向的request對象,通過request我們可以得到url的absoluteString,也就是網頁端的url,我們通過分析這個url可以知道是否需要和網頁端交互,在handleRequest方法里,我們通過判斷url中是否是以ocProject開頭的,如果是,則符合對應的規則,然后繼續解析各個js參數,function是api名,callbackId是回調的id,根據這個id回傳給js,網頁端就可以知道OC處理的是哪個業務,argsAsString為json格式的參數字符串,然后通過function名稱找到對應的api類來處理這個業務。
OC執行js代碼
在api執行完成之后,會把執行結果回調給JSApiManager類,然后JSApiManager負責將執行結果通過webView回傳給網頁端,所以就需要知道webView是如何調用js代碼的,OC提供了一個方法:
stringByEvaluatingJavaScriptFromString
UIViewController類實現
在UIViewController類中,我們有一個webView并實現了UIWebViewDelegate,并引入JSApiManager來處理交互:
- (void)viewDidLoad {
self.apiManager= [[JSApiManageralloc]initWithWebView:self.mainWebView];
}
當我們加載網頁的時候通過UIWebViewDelegate回調方法:
- (BOOL)webView:(UIWebView*)webView shouldStartLoadWithRequest:(NSURLRequest*)request navigationType:(UIWebViewNavigationType)navigationType {
BOOLisHandled = [self.apiManagerhandleRequest:request];
if(isHandled) {
returnNO;
}
returnYES;
}
將request傳給JSApiManager類處理即可。
最后
至此,整個交互過程大概就是這樣子了,網頁端和OC端交互還可以使用iOS7推出的JavaScriptCore,以后有時間再深入學習這個框架,當然還有第三方框架WebViewJavascriptBridge也封裝了這個功能,這里可能語言組織不是很到位,如有不正確的地方,歡迎指正交流。