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:
- 如果发现通过 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;
}