前言
在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也封装了这个功能,这里可能语言组织不是很到位,如有不正确的地方,欢迎指正交流。