美文网首页
iOS中UIWebView和WKWebView中JS和OC交互

iOS中UIWebView和WKWebView中JS和OC交互

作者: 慕诩 | 来源:发表于2020-10-14 14:59 被阅读0次
    //js端代码
    //scope 是和前端约定好的任意字符串,  self.jsContext[@"scope"] = self;保持两边同步就行
    function buttonDivAction() {
        //此处是为了兼容UIWebView和WKWebView两种调用OC方法
        if (typeof window.scope != 'undefined' ||  是安卓端){
            window.scope.scan();
        }else {
            window.webkit.messageHandlers.scan.postMessage({});
            /*
             1.不带参数:
             window.webkit.messageHandlers.scan.postMessage({});
             window.webkit.messageHandlers.scan.postMessage([]);
             但是不能使用window.webkit.messageHandlers.scan.postMessage()方式
             2.带参数
             window.webkit.messageHandlers.senderModel.postMessage({body: 'sender message'});
             window.webkit.messageHandlers.senderModel.postMessage([body: 'sender message']);
             */
        }
    }
    
    function alertAction(message) {
        alert(message);
    }
    
    #import <UIKit/UIKit.h>
    #import <JavaScriptCore/JavaScriptCore.h>
    #import <WebKit/WebKit.h>
    
    @protocol JSObjcDelegate <JSExport>
    -(void)scan;
    @end
    
    @interface ViewController : UIViewController
    
    
    @end
    
    
    @interface WeakScriptMessageDelegate : NSObject<WKScriptMessageHandler>
    
    @property (nonatomic, weak) id<WKScriptMessageHandler> scriptDelegate;
    
    - (instancetype)initWithDelegate:(id<WKScriptMessageHandler>)scriptDelegate;
    
    @end
    
    #import "ViewController.h"
    #import <WebKit/WebKit.h>
    #define KMainWidth ([UIScreen mainScreen].bounds.size.width)
    #define KMainHeight ([UIScreen mainScreen].bounds.size.height)
    
    @interface ViewController ()<WKNavigationDelegate,WKUIDelegate,WKScriptMessageHandler,UIWebViewDelegate,JSObjcDelegate>
    
    @property(nonatomic,strong)WKWebView *mainWebView;
    
    @property(nonatomic,strong)UIWebView *webView;
    
    @property (nonatomic, strong) JSContext *jsContext;
    
    @property (nonatomic, assign) BOOL isWKWebView;
    
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        self.isWKWebView = YES;
        if (self.isWKWebView) {
             [self.view addSubview:self.mainWebView];
        }else {
             [self.view addSubview:self.webView];
        }
        self.view.backgroundColor = [UIColor whiteColor];
    }
    
    - (WKWebView *)mainWebView{
        if (_mainWebView == nil) {
            WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc] init];
            WKUserContentController *userController = [[WKUserContentController alloc] init];
            // [userController addScriptMessageHandler:self name:@"scan"];
            // WeakScriptMessageDelegate 主要是用来解决([userController addScriptMessageHandler:self name:@"scan"];)方式带了的循环引用问题
            [userController addScriptMessageHandler:[[WeakScriptMessageDelegate alloc] initWithDelegate:self] name:@"scan"];
            configuration.userContentController = userController;
            _mainWebView = [[WKWebView alloc] initWithFrame:CGRectMake(0, 0, KMainWidth, KMainHeight) configuration:configuration];
            NSString *path = [[[NSBundle mainBundle] bundlePath]  stringByAppendingPathComponent:@"index.html"];
            NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL fileURLWithPath:path]];
            [_mainWebView loadRequest: request];
            _mainWebView.navigationDelegate = self;
            _mainWebView.UIDelegate = self;
        }
        return _mainWebView;
    }
    
    
    - (UIWebView *)webView {
        if (!_webView) {
            _webView = [[UIWebView alloc] initWithFrame:CGRectMake(0, 0, KMainWidth, KMainHeight)];
            _webView.delegate = self;
            NSString *path = [[[NSBundle mainBundle] bundlePath]  stringByAppendingPathComponent:@"index.html"];
            NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL fileURLWithPath:path]];
            [_webView loadRequest: request];
        }
        return _webView;
    }
    
    - (void)scan{
        dispatch_async(dispatch_get_main_queue(), ^{//适配13.6
            NSLog(@"=========scan调用成功");
            if (self.isWKWebView) {
                //WKWebView中OC调用JS方法并传值
                 [self.mainWebView evaluateJavaScript:@"alertAction('WKWebView-OC调用JS警告窗方法')" completionHandler:^(id _Nullable item, NSError * _Nullable error) {
                       NSLog(@"self.mainWebView evaluateJavaScript:completionHandler:");
                   }];
            }else {
                // UIWebView中OC调用JS方法并传值
                JSValue *Callback = self.jsContext[@"alertAction"];
                [Callback callWithArguments:@[@"UIWebView-OC调用JS警告窗方法"]];
            }
        });
    }
    
    #pragma mark *****UIWebViewDelegate*****
    - (void)webViewDidFinishLoad:(UIWebView *)webView{
        self.jsContext = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
        self.jsContext[@"scope"] = self;
        self.jsContext.exceptionHandler = ^(JSContext *context, JSValue *exceptionValue) {
            context.exception = exceptionValue;
            NSLog(@"异常信息:%@", exceptionValue);
        };
    }
    
    - (void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error{
       NSLog(@"页面加载失败");
    }
    
    #pragma mark WKScriptMessageHandler
    //接收到JS调用的OC方法的回调
    /*
     js端通过window.webkit.messageHandlers.name.postMessage({});调用OC端方法,此处name要和上面[userController addScriptMessageHandler:[[WeakScriptMessageDelegate alloc] initWithDelegate:self] name:@"scan"];设置的name保持一致
     */
    - (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message{
        if ([message.name isEqualToString:@"scan"]) {
            [self scan];
        }
    }
    
    #pragma mark *****WKWebViewDelegate*****
    //当main frame的导航开始请求时,会调用此方法
    - (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(null_unspecified WKNavigation *)navigation {
    }
    
    //当main frame导航完成时,会回调
    - (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation {
      
    }
    
    //当main frame开始加载数据失败时,会回调
    - (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(null_unspecified WKNavigation *)navigation withError:(NSError *)error {
    
    }
    
    
    //接收到警告面板
    //调用JS的alert()方法
    - (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler{
        UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"提示" message:message preferredStyle:UIAlertControllerStyleAlert];
        UIAlertAction *action = [UIAlertAction actionWithTitle:@"ok" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) {
            //此处的completionHandler()就是调用JS方法时,`evaluateJavaScript`方法中的completionHandler
            completionHandler();
        }];
        [alert addAction:action];
        [self presentViewController:alert animated:YES completion:nil];
    }
    
    //接收到确认面板
    //调用JS的confirm()方法
    - (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL result))completionHandler{
        
    }
    
    //接收到输入框
    //调用JS的prompt()方法
    - (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(nullable NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString * _Nullable result))completionHandler{
        
    }
    
    - (void)dealloc{
        [self.mainWebView.configuration.userContentController removeScriptMessageHandlerForName:@"scan"];
    }
    
    
    @end
    
    
    
    @implementation WeakScriptMessageDelegate
    
    - (instancetype)initWithDelegate:(id<WKScriptMessageHandler>)scriptDelegate {
        self = [super init];
        if (self) {
            _scriptDelegate = scriptDelegate;
        }
        return self;
    }
    
    - (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
        if ([self.scriptDelegate respondsToSelector:@selector(userContentController:didReceiveScriptMessage:)]) {
           [self.scriptDelegate userContentController:userContentController didReceiveScriptMessage:message];
        }
    }
    
    @end
    
    

    完整demo放在github=>走你

    //***************************************分割线*******************************************************

    今日遇到一个需求,我们使用第三方app的H5打卡功能,其中H5的打卡是获取他们原生app中定位方法,现在我们需要集成这个H5,但是我们不知道他们原生定位是怎么写的, haha

    我们想通过注入JS方法,让H5调用时调用我们写的获取定位方法,然后回传,安卓写法

    window.em = {
            
            getLocation: function(option){
                var json = mlem.getLocation();
    
                if(!!json){
                    option.success(JSON.parse(json));
                }else{
                    option.fail({
                        errMsg: "获取定位失败"
                    })
                }
            },
    
            checkJsApi: function(func){
                return "getLocation" == func || "dingAuthFunc" == func;
            },
            dingAuthFunc: function(){
                return true;
            },
            ready:function(cb){
                cb();
            }
        }
    

    由于安卓mlem.getLocation()调用原生方法之后能拿到位置返回值,所以可以进行接下来的处理,直接指向回调函数option.success(JSON.parse(json));但是iOS不同,iOS使用WKWebView后,js端只能通过window.webkit.messageHandlers.mlem.postMessage({})方式调用iOS原生方法,在这个方法执行完成后是没有返回值的,如果H5能提供一个方法接受我们的返回值,跟上面讲的交互方式那样,那就很方便,但是现在没有,只能通过option这个里面的回调函数进行回调,但是这个回调方法再哪里执行呢?,只能在iOS拿到定位后执行啊,拿到定位后又没有js方法可调,这个就尴尬了,之前考虑将这个option传到iOS,然后拿到定位后执行,类似如下:

     window.webkit.messageHandlers.mlem.postMessage({body:JSON.stringify(option)});
    

    但是发现js里的回调函数success和fail经过JSON.stringify(option)后都没有了,这里涉及到js的function序列化和反序列化问题,最后还是没有办法正确执行,只能放弃。

    后来想到一个办法,在window.em 下定义一个对象iOSCallBack,保存这个option,在原生获取定位成功后调用这个对象执行回调,于是有:

    - (void)enjectJsToWebView {
        NSString *jsStr = @"\
                window.em = {\
                    iOSCallBack : {},\
                    getLocation: function(option){\
                        iOSCallBack = option;\
                        window.webkit.messageHandlers.mlem.postMessage({});\
                    },\
                    checkJsApi: function(func){\
                        return \"getLocation\" == func || \"dingAuthFunc\" == func;\
                    },\
                    dingAuthFunc: function(){\
                        return true;\
                    },\
                    ready:function(cb){\
                        cb();\
                    },\
                };";
        [self.webView evaluateJavaScript:jsStr completionHandler:^(id _Nullable obj, NSError * _Nullable error) {
            // NSLog(@"%@",error);
        }];
    }
    
    

    这个函数在下面方法里注入

    - (void)webView:(WKWebView *)webView didCommitNavigation:(WKNavigation *)navigation {
        if (self.isAttendance) {
            [self enjectJsToWebView];
        }
    }
    

    然后就是获取完定位后执行JS回调函数了

    #pragma mark WKScriptMessageHandler
    - (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
        if ([message.name isEqualToString:@"mlem"]) {
            CurrentLocation *location= [CurrentLocation sharedInstance];
            [location startMLFramilyLocationAndGetCurrentPlaceInfo];
            location.returnBlock = ^(NSString *currentCityName, NSString *currentDistrict, NSString *currentDetailAddress, NSString *currentPreciseDetailAddress, CLLocationCoordinate2D currentCoordinate){
                NSDictionary *parm = @{
                    @"latitude" : @(currentCoordinate.latitude),
                    @"longitude" : @(currentCoordinate.longitude),
                    @"address" : currentDetailAddress,
                    @"speed" : @(0.5)
                };
                NSString *parmStr = convertToJsonData(parm);
    // 重点地方!!!!!!!!!!
                NSString *js = [NSString stringWithFormat:@"iOSCallBack.success(%@);",parmStr];
                [self.webView evaluateJavaScript:js completionHandler:^(id _Nullable object, NSError * _Nullable error) {
                    if (error) {
                       // NSLog(@"error = %@", error);
                    }else {
                       // NSLog(@"object = %@", object);
                    }
                }];
            };
        }
    }
    

    Tips:

    1. 如果发现通过 document.body.scrollHeight获取的网页内容真实高度不准确,可以先将WKWebView的高度设置为0,然后在执行下面的方法获取高度
      2.设置关于网页中视频播放
      如果是内联视频,比如使用<embed>标签嵌套的视频需要设置
      allowsInlineMediaPlayback属性为YES

    WKWebViewConfiguration *config = [WKWebViewConfiguration new];
    config.allowsInlineMediaPlayback = YES;
    if (@available(iOS 10.0, *)) {
    config.mediaTypesRequiringUserActionForPlayback = NO;
    }
    注:allowsInlineMediaPlayback
    HTML5视频是否内联播放或使用本机全屏控制器是否能播放内联视频iPhone的默认值为false,iPad的默认值为true。
    将此属性设置为true可以内嵌播放视频。 将此属性设置为false以使用本机全屏控制器。
    所以要想播放h5的视频,就必须设置为true,否则无法播放。
    在iOS 10.0之前创建的应用必须使用webkit-playsinline属性。
    这个属性是ios10及其以后才有的,使用时要注意了。

    mediaTypesRequiringUserActionForPlayback:
    确定哪些媒体类型需要用户手势才能开始播放。如果不需要用户操作的就设置为NO就行了。

     [webView evaluateJavaScript:@"document.body.scrollHeight" completionHandler:^(id _Nullable result,NSError *_Nullable error) {
        CGFloat scrollHeight = [result doubleValue];
     }];
    

    最后关于

    JS对象转字符串保留方法,字符串转对象

    var obj = {
                name:"zhangsan",
                age:20,
                say:function(name){
                    console.log("My name is " + (name ? name : this.name));
                },
                hello:function(){
                    console.log("Hello");
                },
                talk:function(name, age){
                    console.log("My name is " + (name ? name : this.name) + ",my age is " + (age ? age : this.age));
                }
        };
    
        function stringifyObj(obj){
            var newObj = {};
            for(var key in obj){
                if(obj.hasOwnProperty(key) && obj[key] instanceof Function){
                    newObj[key] = obj[key].toString().replace(/[\n\t]/g,"");
                    continue;
                }
                newObj[key] = obj[key];
            }
            return JSON.stringify(newObj);
        }
    
    
        function parseObj(strObj){
            var obj = JSON.parse(strObj);
            var funReg = /function\s\(.*\)/;
            for(var key in obj){
                if(funReg.test(obj[key])){
                    try{
                        var fun = (new Function("return " + obj[key]))();
                        if(fun instanceof Function){
                            obj[key] = fun;
                        }
                    }catch(e){
                        console.log(e)
                    }
                }
            }
            return obj;
        }
    
    
    

    相关文章

      网友评论

          本文标题:iOS中UIWebView和WKWebView中JS和OC交互

          本文链接:https://www.haomeiwen.com/subject/ybcwpktx.html