前言
目录
关于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相关目录
总结
讲解的可能有些粗糙,不过附上了大量代码,小伙伴们看了,应该会有所收获的。日后还会对文章进行修改,完善,增加新的内容。还有哪儿不懂的小伙伴,欢迎留言,我会一一解答。