本文主要介绍iOS&Web交互逻辑及示例代码,及给WKWebView增加加载进度条。
一、交互方式
1、使用原生(开发语言:Objective-C)自带的WKWebView类,iOS7之后出了JavaScriptCore.framework用于与JS交互,实现加载H5网页界面。WKWebView控件,可以完全只借助 iOS 自带的框架进行 OC & JS 交互。
二、WKWebView 原生交互原理
1、通过 WKUserContentController把需要观察的 JS 执行函数注册起来。
2、然后通过一个协议方法,将所有注册过的 JS 函数执行的参数传递到此协议方法中。
3、最后在通过WKWebView以下的方法,返回结果给JS。
- (void)evaluateJavaScript:(NSString *)javaScriptString completionHandler:(void (^ _Nullable)(_Nullable id, NSError * _Nullable error))completionHandler;
三、OC端代码示例
(1)继承WKWebView协议代理方法,初始化WKWebView。
① 初始化
@interface TwoPageVC ()<WKNavigationDelegate,WKScriptMessageHandler,UIImagePickerControllerDelegate,UINavigationControllerDelegate>
@property (nonatomic, strong) WKWebView *wWebView;
@property(nonatomic,strong)NSString *webRequestMethodName;
@property(nonatomic,strong)NSString *webResponseMethodName;
@property (strong, nonatomic) UIImagePickerController *picker;
@property(nonatomic,strong)UIImageView *imageV;
@end
@implementation TwoPageVC
- (void)viewDidLoad {
[super viewDidLoad];
self.navigationItem.title = @"webView页面";
self.picker.delegate = self;
self.picker.allowsEditing = YES;
self.view.backgroundColor = KWhiteColor;
WKWebViewConfiguration * config = [[WKWebViewConfiguration alloc]init];
self.wWebView = [[WKWebView alloc]initWithFrame:CGRectZero configuration:config];
self.wWebView.navigationDelegate = self;
self.localHtml = @"OnePageTest";
NSString *path = [NSString stringWithFormat:@"JSOC.bundle/%@",self.localHtml];
NSURL *fileUrl = [[NSBundle mainBundle] URLForResource:path withExtension:@".html"];
[self.wWebView loadRequest:[NSURLRequest requestWithURL:fileUrl]];
[self.view addSubview:self.wWebView];
[self.view addSubview:self.lab];
[self.wWebView mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.right.top.equalTo(self.view);
make.bottom.equalTo(self.lab.mas_top).offset(-SIZEWIDTH_X(16, ScreenWidth));
}];
[self.lab mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.right.equalTo(self.view);
make.bottom.equalTo(self.view.mas_bottom).offset(-SIZEWIDTH_X(50, ScreenWidth));
}];
self.imageV.hidden = YES;
[self.view addSubview:self.imageV];
[self.imageV mas_makeConstraints:^(MASConstraintMaker *make) {
make.centerX.equalTo(self.view.mas_centerX);
make.bottom.equalTo(self.view.mas_bottom).offset(-SIZEWIDTH_X(50, ScreenWidth));
make.size.mas_equalTo(CGSizeMake(SIZEWIDTH_X(50, ScreenWidth), SIZEWIDTH_X(50, ScreenWidth)));
}];
self.userCC = config.userContentController;
//此处相当于监听了JS方法
[self.userCC addScriptMessageHandler:self name:@"test1Click"];
[self.userCC addScriptMessageHandler:self name:@"test2Click"];
[self.userCC addScriptMessageHandler:self name:@"test3Click"];
[self.userCC addScriptMessageHandler:self name:@"test4Click"];
}
-(UILabel *)lab
{
if (!_lab) {
_lab = [[UILabel alloc]init];
_lab.font = [UIFont systemFontOfSize:SIZEWIDTH_X(18, ScreenWidth)];
_lab.textColor = [UIColor redColor];
_lab.textAlignment = NSTextAlignmentCenter;
_lab.text = @"OC用于显示js传给OC的值的label";
}
return _lab;
}
@end
(2)实现WKWebView协议代理方法及自定义方法
① WKWebView开始加载方法
- (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(WKNavigation *)navigation
{
NSLog(@"开始加载");
}
② WKWebView加载完成方法
- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation {
NSLog(@"加载完成");
}
③ WKWebView加载失败方法
- (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(WKNavigation *)navigation withError:(NSError *)error {
NSLog(@"加载失败");
}
④接收JS传值的方法
#pragma mark WKScriptMessageHandler delegate
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message
{
/*
@abstract The name of the message handler to which the message is sent.
message.name JS调用OC的方法名
@abstract The body of the message.
@discussion Allowed types are NSNumber, NSString, NSDate, NSArray,
NSDictionary, and NSNull.
message.body JS给OC传的数据
*/
self.webRequestMethodName = message.name;
self.webResponseMethodName =message.body[@"webResponseMethod"];
_lab.text = message.body[@"data"][@"user_name"];
[self clickMethodsWithWebRequestMethod:self.webRequestMethodName andWebResponseMethod:self.webResponseMethodName];
}
⑤OC根据Web不同的触发方法实现不同的逻辑
#pragma mark ==========不同触发方法实现不同的逻辑
-(void)clickMethodsWithWebRequestMethod:(NSString *)webRequestMethod andWebResponseMethod:(NSString *)webResponseMethod{
self.imageV.hidden = YES;
if ([self.webRequestMethodName isEqualToString:@"test1Click"]) {
// 获取用户名
[self callbackMethondWithWebRequestMethod:self.webRequestMethodName andWebResponseMethod:self.webResponseMethodName andJsonDic:[self dicWithWebRequestMethodName:self.webRequestMethodName andWebResponseMethodName:self.webResponseMethodName andMsg:@"操作成功" andCode:1 andDataDic:@{}]];
}else if ([self.webRequestMethodName isEqualToString:@"test2Click"]){
// 拍照 唤起相机
[self evokeCamerWithCallBackName:webRequestMethod];
}else if ([self.webRequestMethodName isEqualToString:@"test3Click"]){
// 支付宝支付
[self callbackMethondWithWebRequestMethod:self.webRequestMethodName andWebResponseMethod:self.webResponseMethodName andJsonDic:[self dicWithWebRequestMethodName:self.webRequestMethodName andWebResponseMethodName:self.webResponseMethodName andMsg:@"OC接收到Web触发支付宝支付的请求" andCode:2 andDataDic:@{}]];
}else if ([self.webRequestMethodName isEqualToString:@"test4Click"]){
// 微信支付
[self callbackMethondWithWebRequestMethod:self.webRequestMethodName andWebResponseMethod:self.webResponseMethodName andJsonDic:[self dicWithWebRequestMethodName:self.webRequestMethodName andWebResponseMethodName:self.webResponseMethodName andMsg:@"OC接收到Web触发微信支付的请求" andCode:2 andDataDic:@{}]];
}
}
⑥OC回传给js的方法
#pragma mark =======OC回传js
-(void)callbackMethondWithWebRequestMethod:(NSString *)webRequestMethod andWebResponseMethod:(NSString *)webResponseMethod andJsonDic:( NSDictionary*)jsonDic {
NSString *jsonStr = [self gs_jsonStringCompactFormatForDictionary:jsonDic];
[_wWebView evaluateJavaScript:[NSString stringWithFormat:@"%@('%@')",self.webResponseMethodName,jsonStr] completionHandler:^(id _Nullable response, NSError * _Nullable error) {
NSLog(@"webresponse = %@",response);
NSLog(@"webError = %@",error);
}];
}
⑦构造返回JS结果字典
#pragma mark -------构造字典
-(NSDictionary *)dicWithWebRequestMethodName:(NSString *)webRequestMethodName andWebResponseMethodName:(NSString *)webResponseMethodName andMsg:(NSString *)msg andCode:(NSInteger)code andDataDic:(NSDictionary *)dataDic{
if (dataDic.count == 0) {
dataDic = @{};
}
NSDictionary *dic = [NSDictionary dictionary];
dic = @{@"webRequestMethodName":webRequestMethodName,@"webResponseMethodName":webResponseMethodName,@"msg":msg,@"code":@(code),@"data":dataDic};
return dic;
}
⑧返回JS结果字典转换成JSON字符串
#pragma mark -----字典转json字符串
-(NSString *)gs_jsonStringCompactFormatForDictionary:(NSDictionary *)dicJson {
if (![dicJson isKindOfClass:[NSDictionary class]] || ![NSJSONSerialization isValidJSONObject:dicJson]) {
return nil;
}
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:dicJson options:0 error:nil];
NSString *strJson = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
NSLog(@"响应方法回调strJson == %@",strJson);
return strJson;
}
⑨VC销毁时,移除ScriptMessageHandler
- (void)dealloc {
//最后, VC销毁的时候一定要把handler移除
[self.userCC removeScriptMessageHandlerForName:@"test1Click"];
[self.userCC removeScriptMessageHandlerForName:@"test2Click"];
[self.userCC removeScriptMessageHandlerForName:@"test3Click"];
[self.userCC removeScriptMessageHandlerForName:@"test4Click"];
}
⑩以h5按钮触发拍照为例
#pragma mark -------拍照
//初始化
- (UIImagePickerController *)picker
{
if (!_picker) {
_picker = [[UIImagePickerController alloc]init];
_picker.sourceType = UIImagePickerControllerSourceTypeCamera;
}
return _picker;
}
-(UIImageView *)imageV{
if (!_imageV) {
_imageV = [[UIImageView alloc]init];
}
return _imageV;
}
//获取拍照后图片
- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary<NSString *,id> *)info
{
// 获取图片
UIImage *image = info[UIImagePickerControllerOriginalImage];
self.imageV.hidden = NO;
self.imageV.image = image;
// 压缩一下图片再传
NSData *imgData = UIImageJPEGRepresentation(image, 0.001);
NSString *encodedImageStr = [imgData base64EncodedStringWithOptions:NSDataBase64Encoding64CharacterLineLength];
NSString *imageString = [self removeSpaceAndNewline:encodedImageStr];
// 获取图片后返回
[picker dismissViewControllerAnimated:YES completion:^{
[self callbackMethondWithWebRequestMethod:self.webRequestMethodName andWebResponseMethod:self.webResponseMethodName andJsonDic:[self dicWithWebRequestMethodName:self.webRequestMethodName andWebResponseMethodName:self.webResponseMethodName andMsg:@"用户使用了图片" andCode:1 andDataDic:@{@"imageString":imageString}]];
}];
}
// 图片转成base64字符串需要先取出所有空格和换行符
- (NSString *)removeSpaceAndNewline:(NSString *)str
{
NSString *temp = [str stringByReplacingOccurrencesOfString:@" " withString:@""];
temp = [temp stringByReplacingOccurrencesOfString:@"\r" withString:@""];
temp = [temp stringByReplacingOccurrencesOfString:@"\n" withString:@""];
return temp;
}
//按取消按钮时候的功能
- (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker
{
// 返回
[picker dismissViewControllerAnimated:YES completion:^{
[self callbackMethondWithWebRequestMethod:self.webRequestMethodName andWebResponseMethod:self.webResponseMethodName andJsonDic:[self dicWithWebRequestMethodName:self.webRequestMethodName andWebResponseMethodName:self.webResponseMethodName andMsg:@"用户取消使用相机" andCode:4 andDataDic:@{}]];
}];
}
//判断相机是否可用
-(void)evokeCamerWithCallBackName:(NSString *)callBackName{
BOOL isPicker = YES;
AVAuthorizationStatus authStatus = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo];
// 判断相机是否可用
if (authStatus ==AVAuthorizationStatusRestricted || authStatus ==AVAuthorizationStatusDenied ) {
isPicker = NO;
}
if (isPicker) {
[self presentViewController:self.picker animated:YES completion:^{
[self callbackMethondWithWebRequestMethod:self.webRequestMethodName andWebResponseMethod:self.webResponseMethodName andJsonDic:[self dicWithWebRequestMethodName:self.webRequestMethodName andWebResponseMethodName:self.webResponseMethodName andMsg:@"相机调起成功" andCode:2 andDataDic:@{}]];
}];
}else {
// oc调用js
[self callbackMethondWithWebRequestMethod:self.webRequestMethodName andWebResponseMethod:self.webResponseMethodName andJsonDic:[self dicWithWebRequestMethodName:self.webRequestMethodName andWebResponseMethodName:self.webResponseMethodName andMsg:@"当前相机不可用,请检查是否开启了相机隐私权限" andCode:3 andDataDic:@{}]];
}
}
(2)以URL形式加载网页时,增加进度条
①初始化
@interface TwoPageVC ()<WKNavigationDelegate,WKScriptMessageHandler,WKUIDelegate,UIImagePickerControllerDelegate,UINavigationControllerDelegate>
@property(nonatomic,strong)UIProgressView *progressView;//加载进度条
@end
@implementation TwoPageVC
- (void)viewDidLoad {
[super viewDidLoad];
[self.wWebView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://www.baidu.com"]]];
[self.wWebView addSubview:self.progressView];
[self.progressView mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.top.right.equalTo(self.wWebView);
make.height.mas_equalTo(SIZEWIDTH_X(2, ScreenWidth));
}];
}
-(UIProgressView *)progressView{
if (!_progressView) {
_progressView = [[UIProgressView alloc]initWithProgressViewStyle:UIProgressViewStyleDefault];
// 未填充的进度条颜色
_progressView.trackTintColor = [UIColor clearColor];
// 设置进度条的色彩/进度条填充颜色
_progressView.progressTintColor = [UIColor blueColor];
// 设置初始的进度,防止用户进来就懵逼了(微信大概也是一开始设置的10%的默认值)
[_progressView setProgress:0.1 animated:YES];
}
return _progressView;
}
@end
②KVO,监听WKWebView加载网页进度条方法
- (void)viewDidLoad {
[self.wWebView addObserver:self forKeyPath:@"estimatedProgress" options:NSKeyValueObservingOptionNew context:nil];
[self.wWebView addObserver:self forKeyPath:@"title" options:NSKeyValueObservingOptionNew context:nil];
}
③完成KVO监听进度条方法
// 完成监听方法
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if ([object isEqual:self.wWebView] && [keyPath isEqualToString:@"estimatedProgress"]) {
// 进度条
CGFloat newprogress = [[change objectForKey:NSKeyValueChangeNewKey] doubleValue];
NSLog(@"打印进度值:%f", newprogress);
if (newprogress == 1) { // 加载完成
// 首先加载到头
[self.progressView setProgress:newprogress animated:YES];
// 之后0.3秒延迟隐藏
__weak typeof(self) weakSelf = self;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.3 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
weakSelf.progressView.hidden = YES;
[weakSelf.progressView setProgress:0 animated:NO];
});
}else{
// 加载中
self.progressView.hidden = NO;
[self.progressView setProgress:newprogress animated:YES];
}
}else if([object isEqual:self.wWebView] && [keyPath isEqualToString:@"title"]) {
// 标题
self.navigationItem.title = self.wWebView.title;
}else{
// 其他
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
④ VC销毁时,移除监听进度条方法
- (void)dealloc {
[_wWebView removeObserver:self forKeyPath:@"estimatedProgress"];
[_wWebView removeObserver:self forKeyPath:@"title"];
}
三、Web端代码示例
(1)建立一个OnePageTest.html文件,创建4个按钮
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>欢迎来到web与OC的JS交互testDemon</title>
<script type="text/javascript" src="OnePageTest.js">
</script>
</head>
<body>
<!-- 创建不同按钮,实现不同方法 ,test1Click/test2Click/test3Click/test4Click为按钮点击方法-->
<!-- 用div标签 aligns属性实现按钮居中 -->
<br><div align="center" ><button type="submit" style="width:230pt;height:80pt;font-size:20pt" id="test1" onclick="test1Click()">获取用户名</button></div></br>
<br><div align="center"><button type="submit" style="width:230pt;height:80pt;font-size:20pt" id="test2" onclick="test2Click()">拍照</button></div></br>
<br><div align="center"><button type="submit" style="width:230pt;height:80pt;font-size:20pt" id="test3" onclick="test3Click()">支付宝支付</button></div></br>
<br><div align="center"><button type="submit" style="width:230pt;height:80pt;font-size:20pt" id="test4" onclick="test4Click()">微信支付</button></div></br>
<!-- 用于显示oc点击事件的结果 -->
<h1 id=labelId align="center" >h5用于显示OC传过来的参数的label</h1>
</body>
</html>
(2)建立一个OnePageTest.js文件,实现4个按钮的触发方法,并触发传值给OC
// 实现按钮的点击事件
function test1Click(){
document.getElementById("test1");
console.log('h5的test1打出来拉');
user_info = {"webRequestMethod":"test1Click","webResponseMethod":"result2","data":{"user_name":"JSOCTestDemo","age":"18"}};
/*jS给OC传值*/
window.webkit.messageHandlers.test1Click.postMessage(user_info);
}
function test2Click(){
document.getElementById("test2");
console.log('h5的test2打出来拉');
user_info = {"webRequestMethod":"test2Click","webResponseMethod":"result2","data":{"user_name":"拍照","age":"18"}};
/*jS给OC传值*/
window.webkit.messageHandlers.test2Click.postMessage(user_info);
}
function test3Click(){
document.getElementById("test3");
console.log('h5的test3打出来拉');
user_info = {"webRequestMethod":"test3Click","webResponseMethod":"result2","data":{"user_name":"支付宝支付","age":"18"}};
/*jS给OC传值*/
window.webkit.messageHandlers.test3Click.postMessage(user_info);
}
function test4Click(){
document.getElementById("test4");
console.log('h5的test4打出来拉');
user_info = {"webRequestMethod":"test4Click","webResponseMethod":"result2","data":{"user_name":"微信支付","age":"18"}};
/*jS给OC传值*/
window.webkit.messageHandlers.test4Click.postMessage(user_info);
}
(3)创建接收OC返回结果的方法。
//此方法可以动态设置,由Web端调起OC时,传入;也可以双方协议固定方法名。
//OC调用JS带参方法
function result2(a){
console.log('OC调用了JS无参数的方法');
document.getElementById("labelId").innerHTML = "oc调用了js方法 - 参数:"+a;
}