关于iOS中原生和h5交互的知识总结(二)WKWebView

前言

目录

关于iOS中原生和h5交互的知识总结(一)UIWebView
关于iOS中原生和h5交互的知识总结(二)WKWebView

基于WKWebView的实现,请注意以下几点(可以全局搜索1~7查看代码中对应位置)

1.初始化
2.清理缓存
3.进度条
4.代理方法
5.交互*
6.addScriptMessageHandler导致不释放问题*
7.响应localStorage变化事件*

样例

我们写一个基类WebViewController,之后所有用到WKWebView的controller都可以继承这个基类,省着我们再去在每个controller中再去写一些关于webView的初始化,配置,代理等一堆东西了
WebViewController.h

#import 
#import 
@interface WebViewController : UIViewController

//跳转到该controller时需要加载的页面url
@property (nonatomic, copy) NSString *url;
//是否显示导航栏
@property (nonatomic, assign) BOOL hideNavigationBar;
//暴露进度条,方便子类修改样式,如果进度条样式全局统一,可以不暴露
@property (nonatomic, strong) UIProgressView *progressView;
//暴露webView,用于子类loadRequest
@property (nonatomic, strong) WKWebView *webView;
//暴露WKUserContentController,如果素有WKUserContentController操作都在基类中,可以不暴露
@property (nonatomic, strong) WKUserContentController *controller;

@end

WebViewController.m

#import "WebViewController.h"
#import "WebViewConstant.h"
#import "WebLoadingView.h"
#import "NotifyConstant.h"
#import "ProgressPoolSingleton.h"
#import "WeakScriptMessageDelegate.h"

#import "NavigationController.h"
#import "SendTableViewController.h"
#import "LoginTableViewController.h"
#import "VideoBrowseController.h"
#import "MemberAuthStatusViewController.h"

@interface WebViewController ()
@property (nonatomic, assign) CGFloat contentOffsetY;
@end

@implementation WebViewController

- (UIStatusBarStyle)preferredStatusBarStyle
{
    return UIStatusBarStyleLightContent;
}

//默认隐藏状态栏
- (id)init
{
    self = [super init];
    if (self) {
        _hideNavigationBar = YES;
    }
    return self;
}

- (id)initWithCoder:(NSCoder *)aDecoder
{
    self = [super initWithCoder:aDecoder];
    if (self) {
        _hideNavigationBar = YES;
    }
    return self;
}

//释放时,移除KVO和scriptMessageHandler
- (void)dealloc
{
    [self.webView removeObserver:self forKeyPath:@"estimatedProgress"];
    [[NSNotificationCenter defaultCenter] removeObserver:self];
    
    [self.controller removeScriptMessageHandlerForName:@"getToken"];
    [self.controller removeScriptMessageHandlerForName:@"showWebScene"];
    [self.controller removeScriptMessageHandlerForName:@"showSendScene"];
    [self.controller removeScriptMessageHandlerForName:@"exitScene"];
    [self.controller removeScriptMessageHandlerForName:@"showLoginScene"];
    [self.controller removeScriptMessageHandlerForName:@"showVideoPlayScene"];
    [self.controller removeScriptMessageHandlerForName:@"showMemberCertificationScene"];
}

//添加addScriptMessageHandler
//WeakScriptMessageDelegate这个类是防止添加到self,导致不能释放的问题
//6.addScriptMessageHandler导致不释放问题
- (void)addScriptMessageHandler
{
    WeakScriptMessageDelegate *delegate = [[WeakScriptMessageDelegate alloc] initWithDelegate:self];
    [self.controller addScriptMessageHandler:delegate name:@"getToken"];
    [self.controller addScriptMessageHandler:delegate name:@"showWebScene"];
    [self.controller addScriptMessageHandler:delegate name:@"showSendScene"];
    [self.controller addScriptMessageHandler:delegate name:@"exitScene"];
    [self.controller addScriptMessageHandler:delegate name:@"showLoginScene"];
    [self.controller addScriptMessageHandler:delegate name:@"showVideoPlayScene"];
    [self.controller addScriptMessageHandler:delegate name:@"showMemberCertificationScene"];
}

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.contentOffsetY = self.hideNavigationBar == YES ? 20 : 0;
    self.view.backgroundColor = [UIColor colorWithRed:150.0/255.0 green:0 blue:10.0/255.0 alpha:1];
    self.navigationController.delegate = self;

    self.webView.scrollView.delegate = self;
    self.webView.scrollView.bounces = NO;

    // 1.初始化
    WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc] init];
    self.controller = [[WKUserContentController alloc] init];
    [self addScriptMessageHandler];
    
    //7.响应localStorage变化事件
    configuration.userContentController = self.controller;
    configuration.processPool = [ProgressPoolSingleton sharedInstance].processPool;

    self.webView = [[WKWebView alloc] initWithFrame:CGRectMake(0, self.contentOffsetY, self.view.bounds.size.width, self.view.bounds.size.height-20) configuration:configuration];
    self.webView.scrollView.bounces = YES;
    self.webView.UIDelegate = self;
    self.webView.navigationDelegate = self;
    [self.view addSubview:self.webView];
    //3.进度条
    [self.view addSubview:self.progressView];
    //观察加载进度
    [self.webView addObserver:self forKeyPath:@"estimatedProgress"
                      options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld) context:nil];

    if (self.url) {
        [self.webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:self.url]]];
    }
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillShow:) name:UIKeyboardWillHideNotification object:nil];
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillHide:) name:UIKeyboardWillShowNotification object:nil];
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(refresh) name:HRefreshWebVCNotification object:nil];
    [[WebLoadingView sharedInstance] showOnView:self.view];
    
//    [self debugButton];
}

//调试按钮
- (void)debugButton
{
    UIButton *button = [UIButton buttonWithType:UIButtonTypeSystem];
    [button setTitle:@"点我" forState:UIControlStateNormal];
    [button setBackgroundColor:[UIColor greenColor]];
    button.frame = CGRectMake(0, 400, 100, 100);
    [button addTarget:self action:@selector(debugButtonAction) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:button];
}

//2.清理缓存
- (void)debugButtonAction
{
    NSSet *websiteDataTypes = [NSSet setWithObjects:WKWebsiteDataTypeDiskCache,WKWebsiteDataTypeOfflineWebApplicationCache,WKWebsiteDataTypeMemoryCache,WKWebsiteDataTypeLocalStorage,nil];
    NSDate *dateFrom = [NSDate dateWithTimeIntervalSince1970:0];
    
    @weakify(self)
    [[WKWebsiteDataStore defaultDataStore] removeDataOfTypes:websiteDataTypes modifiedSince:dateFrom completionHandler:^{
        @strongify(self)
        [self.webView reload];
    }];
}

- (void)refresh
{
    [self.webView reload];
}

- (void)keyboardWillShow:(NSNotification *)notifycation
{

}

- (void)keyboardWillHide:(NSNotification *)notifycation
{
    
}

//控制是否显示导航条
- (void)navigationController:(UINavigationController *)navigationController willShowViewController:(UIViewController *)viewController animated:(BOOL)animated
{
    if ([viewController isEqual:self]) {
        [self.navigationController setNavigationBarHidden:self.hideNavigationBar animated:YES];
    } else {
        if ([viewController isKindOfClass:WebViewController.class]) {
            [self.navigationController setNavigationBarHidden:((WebViewController *)viewController).hideNavigationBar animated:YES];
        } else {
            [self.navigationController setNavigationBarHidden:YES animated:YES];
        }
    }
}

#pragma mark - KVC
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    if (object == self.webView && [keyPath isEqualToString:@"estimatedProgress"]) {
        CGFloat newprogress = [[change objectForKey:NSKeyValueChangeNewKey] doubleValue];
        if (newprogress == 1) {
            self.progressView.hidden = YES;
            [self.progressView setProgress:0 animated:NO];
        }else {
            self.progressView.hidden = NO;
            [self.progressView setProgress:newprogress animated:YES];
        }
    }
}

//5.交互
#pragma mark - WKScriptMessageHandler
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message
{
    NSDictionary *dictionary = message.body;
    NSString *nativeCallback;
    if (dictionary &&
        [dictionary isKindOfClass:NSDictionary.class]) {
        nativeCallback = dictionary[NATIVE_CALLBACK];
    }
    
    if ([message.name isEqualToString:@"getToken"]) {
        
        NSString *token = [[[LocalSaveManager sharedInstance] getToken] translateN];
        NSString *responseToken;
        
        if (token) {
            responseToken = [NSString stringWithFormat:@"'%@'",token];
        } else {
            responseToken = @"'invalid_token'";
        }
        
        NSString *evaluateJavaScript = [NSString stringWithFormat:@"%@(%@)",nativeCallback,responseToken];
        
        @weakify(self)
        dispatch_async(dispatch_get_main_queue(), ^ {
            @strongify(self)
            [self.webView evaluateJavaScript:evaluateJavaScript completionHandler:^(id _Nullable result, NSError * _Nullable error) {
                NSLog(@"result:%@",result);
                NSLog(@"error:%@",error);
            }];
        });
    } else if ([message.name isEqualToString:@"exitScene"]) {
        [self.navigationController popViewControllerAnimated:YES];
    } else if ([message.name isEqualToString:@"showWebScene"]) {
        WebViewController *webViewController = [[WebViewController alloc] init];
        webViewController.hidesBottomBarWhenPushed = YES;
        BOOL hideNavigationBar = [dictionary[@"hideNavigationBar"] boolValue];
        webViewController.hideNavigationBar = hideNavigationBar;
        webViewController.url = dictionary[@"url"];
        [self.navigationController pushViewController:webViewController animated:YES];
    } else if ([message.name isEqualToString:@"showSendScene"]) {
        NSString *token = [[LocalSaveManager sharedInstance] getToken];
        if (token) {
            SendTableViewController *sendTableViewController = [self.storyboard instantiateViewControllerWithIdentifier:@"SendTableViewController"];
            [self.navigationController pushViewController:sendTableViewController animated:YES];
        } else {
            LoginTableViewController *loginTableViewController = [self.storyboard instantiateViewControllerWithIdentifier:@"LoginTableViewController"];
            NavigationController *nav = [[NavigationController alloc] initWithRootViewController:loginTableViewController];
            [self presentViewController:nav animated:YES completion:nil];
        }
    } else if ([message.name isEqualToString:@"showLoginScene"]) {
        LoginTableViewController *loginTableViewController = [self.storyboard instantiateViewControllerWithIdentifier:@"LoginTableViewController"];
        NavigationController *nav = [[NavigationController alloc] initWithRootViewController:loginTableViewController];
        [self presentViewController:nav animated:YES completion:nil];
    } else if ([message.name isEqualToString:@"showVideoPlayScene"]) {
        VideoBrowseController *videoBrowseController = [[VideoBrowseController alloc] init];
        videoBrowseController.videoUrl = dictionary[@"video_url"];
        videoBrowseController.thumbnail = dictionary[@"thumbnail_url"];
        [self presentViewController:videoBrowseController animated:YES completion:nil];
    } else if ([message.name isEqualToString:@"showMemberCertificationScene"]) {
        NSString *token = [[LocalSaveManager sharedInstance] getToken];
        if (token) {
            MemberAuthStatusViewController *memberAuthStatusViewController = [[UIStoryboard storyboardWithName:@"Service" bundle:nil] instantiateViewControllerWithIdentifier:@"MemberAuthStatusViewController"];
            NavigationController *nav = [[NavigationController alloc] initWithRootViewController:memberAuthStatusViewController];
            [self presentViewController:nav animated:YES completion:nil];
        } else {
            LoginTableViewController *loginTableViewController = [self.storyboard instantiateViewControllerWithIdentifier:@"LoginTableViewController"];
            NavigationController *nav = [[NavigationController alloc] initWithRootViewController:loginTableViewController];
            [self presentViewController:nav animated:YES completion:nil];
        }
    } else {
        
    }
}

//4.代理方法
#pragma mark - WKUIDelegate
- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler
{
    completionHandler();
    UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"提示" message:message preferredStyle:UIAlertControllerStyleAlert];
    UIAlertAction *action = [UIAlertAction actionWithTitle:@"确认" style:UIAlertActionStyleDefault handler:nil];
    [alertController addAction:action];
    [self presentViewController:alertController animated:YES completion:nil];
}

#pragma mark - WKNavigationDelegate
- (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(null_unspecified WKNavigation *)navigation
{
    //    [self rotate];
}

- (void)webView:(WKWebView *)webView didFinishNavigation:(null_unspecified WKNavigation *)navigation
{
    //    [self stopRotation];
    [[WebLoadingView sharedInstance] hide];
}

- (void)webView:(WKWebView *)webView didFailNavigation:(null_unspecified WKNavigation *)navigation withError:(NSError *)error
{
    //    [self stopRotation];
    [[WebLoadingView sharedInstance] hide];
}

#pragma mark - get method
- (UIProgressView *)progressView
{
    if (!_progressView)
    {
        _progressView = [[UIProgressView alloc] initWithFrame:CGRectMake(0, self.contentOffsetY, self.view.bounds.size.width, 2)];
        _progressView.tintColor = [UIColor greenColor];
        _progressView.trackTintColor = [UIColor whiteColor];
    }
    return _progressView;
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

@end

解答1.2.3.4请自行百度吧,这里主要说下5.6.7

5.交互*

6.addScriptMessageHandler导致不释放问题*

WKWebView和UIWebView js与原生交互是完全不同的,我们需要向messageHandlers中注册标识,js端通过这些标识来与原生交互,WeakScriptMessageDelegate是解决问题6,下面会给出代码

WeakScriptMessageDelegate *delegate = [[WeakScriptMessageDelegate alloc] initWithDelegate:self];
[self.controller addScriptMessageHandler:delegate name:@"getToken"];
[self.controller addScriptMessageHandler:delegate name:@"showWebScene"];
[self.controller addScriptMessageHandler:delegate name:@"showSendScene"];
[self.controller addScriptMessageHandler:delegate name:@"exitScene"];
[self.controller addScriptMessageHandler:delegate name:@"showLoginScene"];
[self.controller addScriptMessageHandler:delegate name:@"showVideoPlayScene"];
[self.controller addScriptMessageHandler:delegate name:@"showMemberCertificationScene"];

然后在WKScriptMessageHandler的代理方法中去处理这些标识(完整代码见WebViewController.m),这里着重说一下getToken()这个方法,还记得讲UIWebView的时候吗,token是可以直接return回去的。可是在WKWebView中是没法在下面的代理方法写return的,所以js调用原生后,原生要给js传值,只能通过下面的方法

- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message
{
    NSDictionary *dictionary = message.body;
    NSString *nativeCallback;
    //nativeCallback是js端定义的方法,native端取到js端定义的方法名后带着token去执行js端的这个方法,取到的就是js端定义的 getTokenCallback这个方法名
    if (dictionary &&
        [dictionary isKindOfClass:NSDictionary.class]) {
        nativeCallback = dictionary[NATIVE_CALLBACK];
    }
    
    if ([message.name isEqualToString:@"getToken"]) {
        
       //获取native端的token
        NSString *token = [[[LocalSaveManager sharedInstance] getToken] translateN];
        NSString *responseToken;
        
        if (token) {
            responseToken = [NSString stringWithFormat:@"'%@'",token];
        } else {
            responseToken = @"'invalid_token'";
        }
        
       //拼接js端的方法和native的token作为方法参数,然后执行,把token回传给js端
        NSString *evaluateJavaScript = [NSString stringWithFormat:@"%@(%@)",nativeCallback,responseToken];
        
        @weakify(self)
        dispatch_async(dispatch_get_main_queue(), ^ {
            @strongify(self)
            [self.webView evaluateJavaScript:evaluateJavaScript completionHandler:^(id _Nullable result, NSError * _Nullable error) {
                NSLog(@"result:%@",result);
                NSLog(@"error:%@",error);
            }];
        });
    } 
}

下面给出WeakScriptMessageDelegate类的代码
WeakScriptMessageDelegate.h

#import 
#import 
@interface WeakScriptMessageDelegate : NSObject

@property (nonatomic, weak) id scriptDelegate;

- (instancetype)initWithDelegate:(id)scriptDelegate;

@end

WeakScriptMessageDelegate.m

#import "WeakScriptMessageDelegate.h"

@implementation WeakScriptMessageDelegate

- (instancetype)initWithDelegate:(id)scriptDelegate {
    self = [super init];
    if (self) {
        _scriptDelegate = scriptDelegate;
    }
    return self;
}

- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
    [self.scriptDelegate userContentController:userContentController didReceiveScriptMessage:message];
}

@end

接下来附上js端的调用代码,js端的代码不太想解释了,看不懂的同学多看几眼相信会看懂的

function isAndroid() {
    var u = navigator.userAgent;
    var isAndroid = u.indexOf('Android') > -1 || u.indexOf('Adr') > -1;
    return isAndroid;
}

function isIOS() {
    var u = navigator.userAgent;
    var isIOS = !!u.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/);
    return isIOS;
}

// public
// {
//  request:request,
//  callback:callback,
// }
var getTokenOptionCallback = null;
function getToken(option) {
    option = option || {};
    option.request = option.request || {};
    option.callback = option.callback || function () {};
    option.complete = option.complete || false;
    option.response = option.response || {};

    if (isIOS()) {
        if (option.complete != true) {
            getTokenOptionCallback = option.callback;
            option.request.nativeCallback = "getTokenCallback";
            window.webkit.messageHandlers.getToken.postMessage(option.request);
        } else {

            option.callback = getTokenOptionCallback;
            option.callback(option.response);
        }
    } else if (isAndroid()) {
        var token = jsObject.getToken(option.username);
        option.callback(token);
    }
}

// private (called by native)
// response
// be same with option.request.nativeCallback
function getTokenCallback(response) {
    var complete = true;
    getToken({
        complete:complete,
        response:response,
    });
}

// public , don't need nativeCallback
function showWebScene(url,hideNavigationBar) {
    if (isIOS()) {
        window.webkit.messageHandlers.showWebScene.postMessage({"url":url});
    } else if (isAndroid()) {
        jsObject.showWebScene();
    }
}

// public , don't need nativeCallback
function showSendScene() {
    if (isIOS()) {
        window.webkit.messageHandlers.showSendScene.postMessage(null);
    } else if (isAndroid()) {
        jsObject.showSendScene();
    }
}

// public , don't need nativeCallback
function exitScene() {
    if (isIOS()) {
        window.webkit.messageHandlers.exitScene.postMessage(null);
    } else if (isAndroid()) {
        jsObject.exitScene();
    }
}

// public , don't need nativeCallback
function showLoginScene() {
    if (isIOS()) {
        window.webkit.messageHandlers.showLoginScene.postMessage(null);
    } else if (isAndroid()) {
        jsObject.exitScene();
    }
}

// public , don't need nativeCallback
function showVideoPlayScene(thumbnail_url,video_url) {
    var parameter = {
        "thumbnail_url":thumbnail_url,
        "video_url":video_url
    };
    if (isIOS()) {
        window.webkit.messageHandlers.showVideoPlayScene.postMessage(parameter);
    } else if (isAndroid()) {
        jsObject.showVideoPlayScene(parameter);
    }
}
getToken({
   request:{
      username:'hliu',
      password:'123456',
  },callback:function(token) {
      alert(x+","+token);
  },
});

7.响应localStorage变化事件*

不清楚localStorage事件及用途的同学可以自行百度一下,简单说下我们当时的需求,我们从A界面通过导航控制器跳转到B界面,然后希望,B界面的修改能在A界面中得到响应,在safari中试了,不同窗口间是可以响应localStorage变化事件的,可是在iOS中却死活的不行。后来经过调研终于知道原因,每个WKWebView的实例都会有自己的进程池WKProcessPool,不同进程池中是不能及时响应localStorage变化的,所以现在是让所有的WKWebView使用同一个进程池,就能让不同的WKWebView及时的响应localStorage变化事件

    configuration.userContentController = self.controller;
    configuration.processPool = [ProgressPoolSingleton sharedInstance].processPool;

下面给出ProgressPoolSingleton的代码
ProgressPoolSingleton.h

#import 
#import 
@interface ProgressPoolSingleton : NSObject

+ (instancetype)sharedInstance;

@property (nonatomic, strong) WKProcessPool *processPool;

@end

ProgressPoolSingleton.m

#import "ProgressPoolSingleton.h"

@implementation ProgressPoolSingleton

+ (instancetype)sharedInstance
{
    static ProgressPoolSingleton *progressPoolSingleton;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        progressPoolSingleton = [[ProgressPoolSingleton alloc] init];
    });
    return progressPoolSingleton;
}

- (id)init
{
    self = [super init];
    if (self) {
        self.processPool = [WKProcessPool new];
    }
    return self;
}

@end

附上js样例代码
storage_event.html






Examples






    
数字

storage_dispach.html






Examples





    

    In Page 1
    
    


再附上我的web相关目录


关于iOS中原生和h5交互的知识总结(二)WKWebView_第1张图片
屏幕快照 2018-01-24 下午2.45.45.png

总结

讲解的可能有些粗糙,不过附上了大量代码,小伙伴们看了,应该会有所收获的。日后还会对文章进行修改,完善,增加新的内容。还有哪儿不懂的小伙伴,欢迎留言,我会一一解答。

你可能感兴趣的:(关于iOS中原生和h5交互的知识总结(二)WKWebView)