iOS中UIWebView和WKWebView中JS和OC交互

//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;
    }


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

推荐阅读更多精彩内容