iOS中UIWebView和WKWebView中JS和OC交互

2020-10-14  本文已影响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;
    }


上一篇下一篇

猜你喜欢

热点阅读