iOS&Web 双向交互及WKWebView基础使用

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