OC与JS交互之UIWebView

一、UIWebView介绍

(1)UIWebView提供了三个方法来加载html资源

1、 loadHTMLString:baseURL: 把html文件的内容以字符串的形式加载到webView里面,然后解析。编码为UTF8

NSString *filePath = [[NSBundle mainBundle] pathForResource:@"test" ofType:@"html"];
NSURL *baseURL = [[NSBundle mainBundle] bundleURL];
[self.webView loadHTMLString:[NSString stringWithContentsOfFile:filePath encoding:NSUTF8StringEncoding error:nil] baseURL:baseURL];

2、loadData:MIMEType:textEncodingName:baseURL: 把html文件的内容以二进制的形式加载到webView里面,然后解析。MIMETYPE指定html文件的格式为"text/html",编码为UTF8

NSString *filePath = [[NSBundle mainBundle] pathForResource:@"test" ofType:@"html"];
NSURL *baseURL = [[NSBundle mainBundle] bundleURL];
NSData *data = [NSData dataWithContentsOfFile:filePath];
[self.webView loadData:data MIMEType:@"text/html" textEncodingName:@"UTF-8" baseURL:baseURL];

3、loadRequest:构造一个请求,webView以请求的形式加载本地html文件,然后解析。这种方式可以直接加载网络html

NSString *filePath = [[NSBundle mainBundle] pathForResource:@"index" ofType:@"html"];
NSURL *url = [NSURL fileURLWithPath:filePath];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
[self.webView loadRequest:request];

(2)UIWebView代理

//成为UIWebView的代理,遵守UIWebViewDelegate协议,就能监听UIWebView的加载过程

//UIWebView在发送请求之前,都会调用这个方法,如果返回NO,代表停止加载请求,返回YES,代表允许加载请求
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType;

//开始发送请求(加载数据)时调用这个方法
- (void)webViewDidStartLoad:(UIWebView *)webView;

//请求完毕(加载数据完毕)时调用这个方法
- (void)webViewDidFinishLoad:(UIWebView *)webView;

//请求错误时调用这个方法
- (void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error;

(3)常用属性和方法

// 刷新
- (void)reload;
// 停止加载
- (void)stopLoading;
// 后退函数
- (void)goBack;
// 前进函数
- (void)goForward;
// 是否可以后退
@property (nonatomic, readonly, getter=canGoBack) BOOL canGoBack;
// 是否可以向前
@property (nonatomic, readonly, getter=canGoForward) BOOL canGoForward;
// 是否正在加载
@property (nonatomic, readonly, getter=isLoading) BOOL loading;

二、基于UIWebView"协议拦截"实现的交互方式

(1)JS调用OC

实现原理

1、JS与iOS约定好jsToOc协议,用作JS在调用iOS时url的scheme;
2、点击JS中的按钮加载含有token数据的url:(jsToOc://loginSucceed?js_tokenString);
3、iOS的UIWebView在加载请求前都会调用-webView:shouldStartLoadWithRequest:navigationType:方法来确认是否加载此请求;
4、iOS在此方法内截取jsToOc协议获取JS传过来的数据,执行OC相应的方法,并不允许加载此请求。

 JS代码:

        var loginBtn = document.getElementById("loginID");
        loginBtn.onclick = function () {
            var token = "js_tokenString";
            loginSuccess(token);
        }

        function loginSuccess(token) {
            var action = "loginSuccess";
            jsToOc(action, token);
        }

        //JS调用OC入口
        function jsToOc(action, params) {
            var url = "jsToOc://" + action + "?" + params;
            window.location.href = url;

        } 

 

OC代码:

- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType{
    NSString *absolutePath = request.URL.absoluteString;
    NSString *scheme = request.URL.scheme;
    NSString *host = request.URL.host;
    NSString *query = request.URL.query;
    
    if ([scheme caseInsensitiveCompare:@"jsToOc"] == NSOrderedSame) {
        UIAlertView *alertView = [[UIAlertView alloc]initWithTitle:@"JS调OC01" message:query delegate:self cancelButtonTitle:@"确定" otherButtonTitles:nil, nil];
        [alertView show];
        return NO;
    }
    return YES;
}

除了显示截取到的数据,iOS还可以将request.URL.host看作JS想调用的方法名,将request.URL.query看作该方法的参数集,从而体现出JS调用iOS的概念。

JS代码:

        function btnClick1() {
             window.location.href = "rrcc://showMobile"
        }
        function btnClick2() {
             window.location.href = "rrcc://showName_?xiaohuang"
        }
        function btnClick3() {
             window.location.href = "rrcc://showSendNumber_msg_?13300001111&go climbing this weekend"
        }    

OC代码:

- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType{
    NSString *absolutePath = request.URL.absoluteString;
    NSString *scheme = request.URL.scheme;
    NSString *host = request.URL.host;
    NSString *query = request.URL.query;
    
   if ([scheme caseInsensitiveCompare:@"rrcc"] == NSOrderedSame){
        NSString *schemeStr = @"rrcc://";
        NSString *subPath = [absolutePath substringFromIndex:schemeStr.length];
        if ([subPath containsString:@"?"]) {//1个或多个参数
            if ([subPath containsString:@"&"]) {//多个参数
                NSArray *components = [subPath componentsSeparatedByString:@"?"];
                NSString *methodName = [components firstObject];
                methodName = [methodName stringByReplacingOccurrencesOfString:@"_" withString:@":"];
                SEL sel = NSSelectorFromString(methodName);
                NSString *parameter = [components lastObject];
                NSArray *params = [parameter componentsSeparatedByString:@"&"];
                if (params.count == 2) {
                    if ([self respondsToSelector:sel]) {
                        [self performSelector:sel withObject:[params firstObject] withObject:[params lastObject]];
                    }
                }
            }else{//1个参数
                NSArray *components = [subPath componentsSeparatedByString:@"?"];
                
                NSString *methodName = [components firstObject];
                methodName = [methodName stringByReplacingOccurrencesOfString:@"_" withString:@":"];
                SEL sel = NSSelectorFromString(methodName);
                NSString *parameter = [components lastObject];
                if ([self respondsToSelector:sel]) {
                    [self performSelector:sel withObject:parameter];
                }
            }
        }else{//没有参数
            NSString *methodName = [subPath stringByReplacingOccurrencesOfString:@"_" withString:@":"];
            SEL sel = NSSelectorFromString(methodName);
            
            if ([self respondsToSelector:sel]) {
                [self performSelector:sel];
            }
        }
        return NO;
    }
    return YES;
}


- (void)showMobile{
    NSLog(@"showMobile");
}
- (void)showName:(NSString *)name{
    NSLog(@"showName:%@", name);
   
}

- (void)showSendNumber:(NSString *)phone msg:(NSString *)msg{
    NSLog(@"showSendNumbermsg:==phone:%@===msg:%@", phone, msg);
}

(2)OC调用JS

OC使用UIWebView的-stringByEvaluatingJavaScriptFromString:方法访问JS;

JS代码:

        function alertMobile() {
            alert('alertMobile 我是手机号是:13300001111')
        }
    
        function alertName(msg) {
            alert('alertName 你好 ' + msg + ', 我也很高兴见到你')
        }    

OC代码:

- (void)btnAction:(UIButton *)sender{
    if (sender.tag == 123) {
        [self.webView stringByEvaluatingJavaScriptFromString:@"alertMobile()"];
    }
    
    if (sender.tag == 234) {
        [self.webView stringByEvaluatingJavaScriptFromString:@"alertName('小红')"];
    }
}

UIWebView的-stringByEvaluatingJavaScriptFromString:方法可以执行JS代码。但只有在整个webView加载完成之后调用此方法才会有响应。

三、OC与JS交互之JavaScriptCore

JavaScriptCore提供了JavaScript和Objective-C桥接的Obj-C API。JavaScriptCore提供了让我们脱离UIWebView执行JavaScript脚本的能力,以及使用现代的Objective-C语法(例如Blocks和下标)在Objective-C和JavaScript之间无缝的传递值或者对象。

(1)JS调用OC

原理:

1、JS与iOS约定好jsToOc方法,作为JS调用iOS的入口;
2、iOS通过[webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"]获取JS代码的上下文JSContext;
3、JS调用jsToOc方法,iOS使用Block形式监听(重写)此方法context[@"jsToOc"] = ^() {},收到JS的调用请求和参数;

JS代码:

        var loginBtn = document.getElementById("loginID");
        loginBtn.onclick = function () {
            var token = "js_tokenString";
            loginSuccess(token);
        }

        //登录成功
        function loginSuccess(token) {
            var action = "loginSuccess";
            jsToOc(action, token);

        }

        //JS调用OC入口
        function jsToOc(action, params) {
            var url = "jsToOc://" + action + "?" + params;
            window.location.href = url;
        }
function btnClick1() {
//showMobile(); window.location.href = "rrcc://showMobile"; } function btnClick2() { showName('xiaohuang') } function btnClick3() { showSendMsg('13300001111', 'Go Climbing This Weekend !!!') }

OC代码:

#import <JavaScriptCore/JavaScriptCore.h>
//UIWebView在每次加载请求完成后会调用此方法
- (void)webViewDidFinishLoad:(UIWebView *)webView{
    
    //获取JS代码的执行环境/上下文/作用域
    JSContext *context = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
    //监听JS代码里面的方法(执行效果上可以理解成重写了JS的方法)
    context[@"jsToOc"] = ^(NSString *action, NSString *param){
        dispatch_async(dispatch_get_main_queue(), ^{
            UIAlertView *alertView = [[UIAlertView alloc]initWithTitle:action message:param delegate:self cancelButtonTitle:@"确定" otherButtonTitles:nil, nil];
            [alertView show];
        });
    };
    
    context[@"btnClick1"] = ^{
        dispatch_async(dispatch_get_main_queue(), ^{
            UIAlertView *alertView = [[UIAlertView alloc]initWithTitle:@"showMobile" message:@"showMobile" delegate:self cancelButtonTitle:@"确定" otherButtonTitles:nil, nil];
            [alertView show];
        });
    };
    
//    context[@"showName"] = ^(NSString *name){
//        dispatch_async(dispatch_get_main_queue(), ^{
//            UIAlertView *alertView = [[UIAlertView alloc]initWithTitle:@"showName" message:name delegate:self cancelButtonTitle:@"确定" otherButtonTitles:nil, nil];
//            [alertView show];
//        });
//    };
    
    void(^showName)(NSString *name) = ^(NSString *name){
            dispatch_async(dispatch_get_main_queue(), ^{
                UIAlertView *alertView = [[UIAlertView alloc]initWithTitle:@"showName" message:name delegate:self cancelButtonTitle:@"确定" otherButtonTitles:nil, nil];
                [alertView show];
            });
    };
    [context setObject:showName forKeyedSubscript:@"showName"];
    
    context[@"showSendMsg"] = ^(NSString *phone, NSString *msg){
        dispatch_async(dispatch_get_main_queue(), ^{
            UIAlertView *alertView = [[UIAlertView alloc]initWithTitle:phone message:msg delegate:self cancelButtonTitle:@"确定" otherButtonTitles:nil, nil];
            [alertView show];
        });
    };
}

(2)OC调JS

JS代码:

        function ocToJs(action, params) {
            document.getElementById("returnValue").innerHTML = action + '?' + params;
        }
    
        function alertMobile() {
            alert('alertMobile 我是手机号是:13300001111')
        }
    
        function alertName(msg) {
            alert('alertName 你好 ' + msg + ', 我也很高兴见到你')
        }
    
        function alertSendMsg(num,msg) {
            alert('alertSendMsg这是我的手机号:' + num + ',' + msg + '!!')
        }    

OC代码:

#pragma mark - Action
- (void)loginBtn01Action:(UIButton *)btn{
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        JSContext *context = [self.webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
        
        [context evaluateScript:[NSString stringWithFormat:@"ocToJs('loginSucceed', 'oc_tokenString')"]];
        
    });
}

- (void)btnAction:(UIButton *)sender{
    if (sender.tag == 123) {
        //[self.webView stringByEvaluatingJavaScriptFromString:@"alertMobile()"];
        JSContext *context = [self.webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
        [context evaluateScript:[NSString stringWithFormat:@"alertMobile()"]];
        
    }
    
    if (sender.tag == 234) {
        //[self.webView stringByEvaluatingJavaScriptFromString:@"alertName('小红')"];
        JSContext *context = [self.webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
        [context evaluateScript:[NSString stringWithFormat:@"alertName('小红')"]];
    }
    
    if (sender.tag == 345) {
        //[self.webView stringByEvaluatingJavaScriptFromString:@"alertSendMsg('18870707070','周末爬山真是件愉快的事情')"];
        JSContext *context = [self.webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
        [context evaluateScript:[NSString stringWithFormat:@"alertSendMsg('18870707070','周末爬山真是件愉快的事情')"]];
    }
}

除了使用JSContext的-evaluateScript:方法之外,还可以先通过[context[@"ocToJs"]获取到JS的ocToJs方法对应的JSValue,然后使用JSValue的-callWithArguments:方法调用JS的ocToJs方法。

四、OC与JS交互之JSExport协议

下面介绍如何使用JSExport协议在 UIWebView上实现OC与JS交互。JSExport是JavaScriptCore框架里的一个协议。如果一个协议遵守了JSExport,那么该协议的方法会对JS开放,允许JS直接调用。

JS调OC

实现原理:

1、JS与iOS约定好OCJSBridge类名和jsToOc方法,作为JS调用iOS的入口OCJSBridge.jsToOc;
2、iOS创建遵守JSExport协议的OCJSExport协议,在该协议中声明-jsToOc:params:方法(起个别名jsToOc);
3、UIWebViewJSExportController遵守OCJSExport协议,实现该协议中的-jsToOc:params:方法;
4、iOS通过[webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"]获取JS代码的上下文JSContext;
5、iOS通过context[@"OCJSBridge"] = self;在context注册OCJSBridge对象为self(UIWebViewJSExportController的实例);
6、JS调用OCJSBridge.jsToOc方法,iOSUIWebViewJSExportController的-jsToOc:params:方法会响应JS的调用请求和参数;

JS代码:

        var loginBtn = document.getElementById("loginID");
        loginBtn.onclick = function () {
            var token = "js_tokenString";
            loginSuccess(token);
        }

        //登录成功
        function loginSuccess(token) {
            var action = "loginSuccess";
            OCJSBridge.jsToOc(action, token);
        }
       
    
        function btnClick1() {
            OCJSBridge.showMobile();
        }
    
        function btnClick2() {
            OCJSBridge.showName('xiaohuang');
        }
    
        function btnClick3() {
            OCJSBridge.showSendMsg('13300001111', 'Go Climbing This Weekend !!!');
        }
        

 

OC代码

#import <JavaScriptCore/JavaScriptCore.h>

 

@protocol OCJSExport <JSExport>

 

//为OC的-jsToOC:params:方法起个JS认识的别名jsToOc

JSExportAs(jsToOc, - (void)jsToOc:(NSString *)action params:(NSString *)params);

JSExportAs(showSendMsg, - (void)showSendMsg:(NSString *)phone params:(NSString *)msg);

JSExportAs(showName, - (void)showName:(NSString *)name);

//无参数和单参数时可不起别名

- (void)showMobile;

 

@end

//UIWebView在每次加载请求完成后会调用此方法
- (void)webViewDidFinishLoad:(UIWebView *)webView{
    
    //获取JS代码的执行环境/上下文/作用域
    JSContext *context = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
    //! 在context注册OCJSBridge对象为self
    context[@"OCJSBridge"] = self;//!< 有循环引用问题
    
}
//! 实现OCJSExport协议的方法
- (void)jsToOc:(NSString *)action params:(NSString *)params {
    
    dispatch_async(dispatch_get_main_queue(), ^{
        UIAlertView *alertView = [[UIAlertView alloc]initWithTitle:action message:params delegate:self cancelButtonTitle:@"确定" otherButtonTitles:nil, nil];
        [alertView show];
    });
}

- (void)showMobile{
    dispatch_async(dispatch_get_main_queue(), ^{
        UIAlertView *alertView = [[UIAlertView alloc]initWithTitle:@"showMobile" message:@"showMobile" delegate:self cancelButtonTitle:@"确定" otherButtonTitles:nil, nil];
        [alertView show];
    });
}

- (void)showName:(NSString *)name{
    dispatch_async(dispatch_get_main_queue(), ^{
        UIAlertView *alertView = [[UIAlertView alloc]initWithTitle:@"提示" message:name delegate:self cancelButtonTitle:@"确定" otherButtonTitles:nil, nil];
        [alertView show];
    });
}

- (void)showSendMsg:(NSString *)phone params:(NSString *)msg{
    dispatch_async(dispatch_get_main_queue(), ^{
        UIAlertView *alertView = [[UIAlertView alloc]initWithTitle:phone message:msg delegate:self cancelButtonTitle:@"确定" otherButtonTitles:nil, nil];
        [alertView show];
    });
}

1、JS的方法命名规则与OC不一样,当OCJSExport协议中的方法有多个参数时,需要使用JSExportAs(<#PropertyName#>, <#Selector#>)为OC方法起个别名。否则,JS就需要很别扭的使用OCJSBridge.jsToOcParams来调用OC的-jsToOc:params:方法(无参数和单参数时可不起别名)。
2、context[@"OCJSBridge"] = self;会有循环引用问题,导致self的-dealloc方法不被执行。因为JS中没有弱引用,所以__weak在这里不起作用。一般来说,可以使用单独的类来处理OCJSExport协议的相关方法,以解决此问题(比如:context[@"OCJSBridge"] = [OCJSBridge new];)。

 

参考:https://www.jianshu.com/p/f94448235209

   https://www.cnblogs.com/markstray/p/5757238.html

   https://www.jianshu.com/p/f4ec947f0721

   https://www.cnblogs.com/markstray/p/5757255.html

GitHub:https://github.com/hongsheng1024/RunTime

 

posted @ 2018-10-30 23:08  hongsheng  阅读(635)  评论(0编辑  收藏  举报