OC和JS交互实践篇

前言

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容

  • 前言 关于UIWebView的介绍,相信看过上文的小伙伴们,已经大概清楚了吧,如果有问题,欢迎提问。 本文是本系列...
    CoderLF阅读 8,944评论 2 12
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,519评论 25 707
  • 我丢失了一个梦境 昨夜凌晨两点 我把它挂在床边 它生性善变 从来谎话连篇 时而松脆时而绵软 凌晨六点它已不见 没有...
    懒懒的小玉阅读 283评论 5 4
  • 月满中秋 自古以来,明月便是中国诗歌中的常客,古往今来,多少诗人曾发出对月的...
    如斯mcc阅读 262评论 0 0
  • 前些天弄Genymotion,毕竟现在Android6.0和7.0大部分手机都已经覆盖了,所以在测试过程中弄一个6...
    茴香豆的第五种写法阅读 681评论 0 1