iOS OC与JS交互实战

     今天看见有人反对开源的,反对技术分享的,说这是方便了其他开发者,或者说最终是方便了资本家。但是我想说的是我也是从大家的技术分享中一点一点积累的技能,还是需要感谢先驱者的无私分享。
     好了OC与JS交互是一个老的技术点,但是我发现网上大多数的文章都是互相抄,千篇一律,没有将里面的关系完全串联讲透。这个功能在开发的时候,避免不了是需要和Web前端沟通的,关键是前端可能也没有开发过这个功能,如果你也能知道一点点前端相关的知识点的话,工作效率会事半功倍。
     这篇文章,我不会去装逼的讨论,到底有几种交互方式,那种交互方式好,还是比较贴近实战的MessageHandler方式来交互。如果你觉得看不懂我的,可以去其他基础教程看完,再来看我的,你可能会茅塞顿开。我这里只是一个总结。

交互方式:一条线是从JS走向OC,一条线是从OC走到JS

我们先从JS走向OC
一:JS调用OC的方法

前端部分:
也就是JS的方法去调用OC的方法,比如web页面里面有一个按钮,点击到了web的按钮,触发了按钮的方法,此js方法再调到OC这边来,触发OC的协议方法进行处理.

html代码(UI布局)

<p style="text-align:center"> <button id="btn1" type = "button" onclick = "jsToOcFunction1()" > JS调用OC:不带参数  </button> </p>

<p style="text-align:center"> <button id="btn2" type = "button" onclick = "jsToOcFunction2()"> JS调用OC:带参数  </button> </p>

JS代码:

function jsToOcFunction1()
{
 window.webkit.messageHandlers.jsToOcNoPrams.postMessage({});
}

function jsToOcFunction2()
{
 window.webkit.messageHandlers.jsToOcWithPrams.postMessage({"params":"我是参数"});
}

不要抗拒查看前端的代码,这能方便你更好的理解交互的全过程。

1.你可以看到html部分有两个button的标签,他们会分别触发两个方法jsToOcFunction1()和jsToOcFunction2(),一个是携带参数的,一个是不懈怠参数的。在这两个方法的内部它们开始调用OC.比如这句JS代码window.webkit.messageHandlers.jsToOcWithPrams.postMessage({“params”:“我是参数”}); 其中的webkit是指的苹果的内核,好像也就是苹果的浏览器,webkit前端是打点,点不出来的,硬写!webkit是否是苹果的内核也不是每个前端都知道的,有些没有接触过的前端就不知道。可能你也发现了,这个方法的调用前端是需要判断是安卓还是苹果的。不要听他们说,安卓都通了,前端应该就没问题了,我们苹果是不一样的。

2.其中的jsToOcNoPrams和jsToOcWithPrams就是调到OC的方法名字了,前端也是硬写的,不需要在其他地方定义的。你可能会问,一个没有定义的方法,可以调用吗?? 答案是“可以!”。这是webKit内置的,在我们的WKWebView环境下是不会报错的,语法是通过的。如果是在普通的JS环境下就会报错了,你可以让前端去试一试.

3.这也是一段前端是实战代码片段,框起来的是调用ios的,它进行了环境的判断refill为OC端命名的方法名字.
iOS OC与JS交互实战

OC部分
好了,知道了前端如何操作的,该我们iOS上了。我们iOS代码还不是驾轻就熟。这里和网上其他资料千篇一律,但是还是有不一样需要注意的地方,WKWebView在注册交互JS方法后,你即使在dealloc里面移除交互对象,它的内存其实也不会释放,这里我们需要一个中间层去绕一绕,绕回来后就不会造成WKWebView内存不释放的问题.

一。我们定义曲线救国的中间层

#import <Foundation/Foundation.h>
#import <WebKit/WebKit.h>

NS_ASSUME_NONNULL_BEGIN

// WKWebView 内存不释放的问题解决
@interface WeakWebViewScriptMessageDelegate : NSObject<WKScriptMessageHandler>

// WKScriptMessageHandler 这个协议类专门用来处理JavaScript调用原生OC的方法
@property (nonatomic, weak) id<WKScriptMessageHandler> scriptDelegate;

- (instancetype)initWithDelegate:(id<WKScriptMessageHandler>)scriptDelegate;

@end

```objectivec
NS_ASSUME_NONNULL_END

#import “WeakWebViewScriptMessageDelegate.h”

@implementation WeakWebViewScriptMessageDelegate

  • (instancetype)initWithDelegate:(id)scriptDelegate {
    self = [super init];
    if (self) {
    _scriptDelegate = scriptDelegate;
    }
    return self;
    }

#pragma mark - WKScriptMessageHandler
//遵循WKScriptMessageHandler协议,必须实现如下方法,然后把方法向外传递
//通过接收JS传出消息的name进行捕捉的回调方法

  • (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
    if ([self.scriptDelegate respondsToSelector:@selector(userContentController:didReceiveScriptMessage:)]) {
    [self.scriptDelegate userContentController:userContentController didReceiveScriptMessage:message];
    }
    }

@end


二 我们在控制器中初始化WKWebView,注册方法.

1.初始化

```objectivec
//创建网页配置对象.
    WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc] init];
    //是使用h5的视频播放器在线播放, 还是使用原生播放器全屏播放
    configuration.allowsInlineMediaPlayback = YES;
    //设置视频是否需要用户手动播放  设置为NO则会允许自动播放
    if (@available(iOS 10.0, *)) {
        configuration.mediaTypesRequiringUserActionForPlayback = YES;
    } else {}
    //设置是否允许画中画技术 在特定设备上有效
    configuration.allowsPictureInPictureMediaPlayback = YES;
    //设置请求的User-Agent信息中应用程序名称 iOS9后可用
    configuration.applicationNameForUserAgent = @"ChinaDailyForiPad";
    
   //自定义的WKScriptMessageHandler 是为了解决内存不释放的问题.
    WeakWebViewScriptMessageDelegate *weakScriptMessageDelegate = [[WeakWebViewScriptMessageDelegate alloc] initWithDelegate:self];
    //这个类主要用来做native与JavaScript的交互管理
    WKUserContentController * wkUController = [[WKUserContentController alloc] init];
    //注册一个name为jsToOcNoPrams的js方法 设置处理接收JS方法的对象
    [wkUController addScriptMessageHandler:weakScriptMessageDelegate  name:@"jsToOcNoPrams"];
    [wkUController addScriptMessageHandler:weakScriptMessageDelegate  name:@"jsToOcWithPrams"];
    configuration.userContentController = wkUController;
    
   //创建设置对象.
    WKPreferences *preference = [[WKPreferences alloc]init];
    //最小字体大小 当将javaScriptEnabled属性设置为NO时,可以看到明显的效果
    preference.minimumFontSize = 0;
    //设置是否支持javaScript 默认是支持的
    preference.javaScriptEnabled = YES;
    //在iOS上默认为NO,表示是否允许不经过用户交互由javaScript自动打开窗口
    preference.javaScriptCanOpenWindowsAutomatically = YES;
    configuration.preferences = preference;
    
   self.webView = [[WKWebView alloc] initWithFrame:CGRectMake(0, 0, [UIScreen mainScreen].bounds.size.width, [UIScreen mainScreen].bounds.size.height) configuration:configuration];
    //UI代理.
    self.webView.UIDelegate = self;
    //导航代理.
    self.webView.navigationDelegate = self;
    //是否允许手势左滑返回上一级, 类似导航控制的左滑返回.
    self.webView.allowsBackForwardNavigationGestures = YES;

2 销毁

- (void)dealloc{
    //移除注册的js方法
    [[_webView configuration].userContentController removeScriptMessageHandlerForName:@"jsToOcNoPrams"];
    [[_webView configuration].userContentController removeScriptMessageHandlerForName:@"jsToOcWithPrams"];
    //移除观察者
    [_webView removeObserver:self
                  forKeyPath:NSStringFromSelector(@selector(estimatedProgress))];
    [_webView removeObserver:self
                  forKeyPath:NSStringFromSelector(@selector(title))];
}

3.实现回调协议

js调到OC的方法,都会走这个协议,我们需要去判断协议中的message.name是否是我们定义的这个方法,是这个方法我们再去接收值,值在message.body里面,是个字典。

#pragma mark - js在调用方法的时候,js方法中再调用OC的方法,会来到此协议.(这种情景一般是用户点击触发了某种交互)
//被自定义的WKScriptMessageHandler在回调方法里通过代理回调回来,绕了一圈就是为了解决内存不释放的问题.
//通过接收JS传出消息的name进行捕捉的回调方法
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message{
    NSLog(@"name:%@\\\\n body:%@\\\\n frameInfo:%@\\\\n",message.name,message.body,message.frameInfo);
    //用message.body获得JS传出的参数体,message.body就是js那边传出来的所有参数,都携带在这里.
    NSDictionary * parameter = message.body;
    //JS调用OC
    if([message.name isEqualToString:@"jsToOcNoPrams"]){
        UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"js调用到了oc" message:@"不带参数" preferredStyle:UIAlertControllerStyleAlert];
        [alertController addAction:([UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
        }])];
        [self presentViewController:alertController animated:YES completion:nil];
    }else if([message.name isEqualToString:@"jsToOcWithPrams"]){
        UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"js调用到了oc" message:parameter[@"params"] preferredStyle:UIAlertControllerStyleAlert];
        [alertController addAction:([UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
        }])];
        [self presentViewController:alertController animated:YES completion:nil];
    }
}

你可以看到其中的jsToOcNoPrams和jsToOcWithPrams这两个方法就是我们在原生和js中调用的方法,就可以处理逻辑了.

二.处理JS中的弹出框

JS中的几类弹出框,iOS都有相应的协议去回调和处理,一种弹出框就对应一个协议,可能这个有点麻烦,但是苹果就是这么做的。

我们先看OC中的代码处理, 里面都有注释:

pragma mark - WKUIDelegate js里面会有弹出框,这是处理js各种弹出框的交互.
/**
 *  web界面中有弹出警告框时调用
 *
 *  @param webView           实现该代理的webview
 *  @param message           警告框中的内容
 *  @param completionHandler 警告框消失调用
 */
// 这个是js的弹出框 alert.
- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler {
    UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"HTML的弹出框" message:message?:@"" preferredStyle:UIAlertControllerStyleAlert];
    [alertController addAction:([UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
        completionHandler();
    }])];
    [self presentViewController:alertController animated:YES completion:nil];
}
// 确认框.
//JavaScript调用confirm方法后回调的方法 confirm是js中的确定框,需要在block中把用户选择的情况传递进去.
- (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL))completionHandler{
    UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"" message:message?:@"" preferredStyle:UIAlertControllerStyleAlert];
    [alertController addAction:([UIAlertAction actionWithTitle:@"关闭" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) {
        completionHandler(NO);
    }])];
    [alertController addAction:([UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
        completionHandler(YES);
    }])];
    [self presentViewController:alertController animated:YES completion:nil];
}
// 输入框.
//JavaScript调用prompt方法后回调的方法 prompt是js中的输入框 需要在block中把用户输入的信息传入.
- (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString * _Nullable))completionHandler{
    UIAlertController *alertController = [UIAlertController alertControllerWithTitle:prompt message:@"" preferredStyle:UIAlertControllerStyleAlert];
    [alertController addTextFieldWithConfigurationHandler:^(UITextField * _Nonnull textField) {
        textField.text = defaultText;
    }];
    [alertController addAction:([UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
        completionHandler(alertController.textFields[0].text?:@"");
    }])];
    [self presentViewController:alertController animated:YES completion:nil];
}
// 打开新窗口,页面是弹出窗口blank处理,blank是js中"打开一个新窗口"的意思.
- (WKWebView *)webView:(WKWebView *)webView createWebViewWithConfiguration:(WKWebViewConfiguration *)configuration forNavigationAction:(WKNavigationAction *)navigationAction windowFeatures:(WKWindowFeatures *)windowFeatures {
    if (!navigationAction.targetFrame.isMainFrame) {
        [webView loadRequest:navigationAction.request];
    }
    return nil;
}

我们再看看JS端的代码,前端是如何调用弹出框的:

这里是html进行UI布局

<p style="text-align:center"> <button id="btn3" type = "button" onclick = "showAlert()" > oc捕获到html的弹出框  </button> </p>

这里是JS调用弹出框

function showAlert()
    {
        alert("被OC截获到了");
    }

这里的alert就是JS的其中的一种调用弹出框.

三.处理跳转链接(JS中的a标签等)

解释一下,前端网页中不仅仅是调用方法或者调用弹出框触发事件的,JS中的a标签,标签中又有href资源的话,用户直接点击不用调用方法就可以进行跳转,触发跳转的事件.

这就是其中的a标签,表现在UI上就是一个“Github主页”的文本,点击直接进行跳转

<p style="text-align:center"> <a href="github://callName_?https://github.com/wsl2ls">Github主页</a> :通过截获URL调用OC</p>
// 根据WebView对于即将跳转的HTTP请求头信息和相关信息来决定是否跳转.
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
    NSString * urlStr = navigationAction.request.URL.absoluteString;
    NSLog(@"发送跳转请求:%@",urlStr);
    //自己定义的协议头
    if([urlStr hasPrefix:@"github://"]){
        UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"通过截取URL调用OC" message:@"你想前往我的Github主页?" preferredStyle:UIAlertControllerStyleAlert];
        [alertController addAction:([UIAlertAction actionWithTitle:@"取消" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) {
            
        }])];
        [alertController addAction:([UIAlertAction actionWithTitle:@"打开" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
            NSURL *url = [NSURL URLWithString:[urlStr stringByReplacingOccurrencesOfString:@"github://callName_?" withString:@""]];
            if (@available(iOS 10.0, *)) {
                [[UIApplication sharedApplication] openURL:url options:@{} completionHandler:^(BOOL success) {
                }];
            } else {
                [[UIApplication sharedApplication] openURL:url];
            }
        }])];
        [self presentViewController:alertController animated:YES completion:nil];
        decisionHandler(WKNavigationActionPolicyCancel);//不允许跳转
    }else if ([urlStr hasPrefix:@"https://www.jianshu.com"]){//简书主页
        decisionHandler(WKNavigationActionPolicyAllow);//允许跳转
    }else{
        decisionHandler(WKNavigationActionPolicyAllow);
    }
}

这个协议就是与原生交互的第三种方式,例如web页面有很多标签等,就是不需要调用方法所触发的事件.明白了这个这下就比较好与前端沟通了,你就问前端,你是调方法呢还是不调方法呢,是不是个a标签直接跳转呢?就这样。

现在我们从OC走向JS

//changeColor()是JS方法名,completionHandler是异步回调block.
    NSString *jsString = [NSString stringWithFormat:@"changeColor('%@')", @"Js颜色参数"];
    [_webView evaluateJavaScript:jsString completionHandler:^(id _Nullable data, NSError * _Nullable error) {
        NSLog(@"改变HTML的背景色");
    }];

JS是弱类型语言,所有的function方法中的参数都不需要声明参数类型,TS需要。所以在上面的字符串中changeColor(’%@’)是不需要书写参数类型的,直接按照前端声明的方法和参数传递的顺序将前端需要的值传递进去就可以了.

//OC调用JS改变背景色
    function changeColor(parameter)
    {
        document.body.style.backgroundColor = randomColor();
    }

好的,再见

上一篇:iOS中js调用oc获取返回值(WKWebView)


下一篇:OC 基础 UICollectionView