iOS-使用WKWebview实现新闻详情页(JS和OC交互)
前言:
这篇文章主要讲解的是网易新闻详情页的大致实现流程,当然也适用于其他新闻软件,本文主要是采用WKWebview
来加载html
文件,里面有本地的CSS
文件控制网页元素样式以及本地JS
文件去实现动态效果、动态数据处理。
监听到用户操作网页里面元素的时候,使用JS
调用Native
里面的方法进行视频播放以及图片浏览,大致也就这些东西,本文重点不是讲解html、css、js
,而是主要讲解如何去解析网易后台返回的数据以及如何去把这些数据拼接到网页里面进行展示,另外还会重点讲解WKWebview
里面关于OC
和JS
交互相关的一些属性和代理方法,并用实际的例子去详细说明它们。
-
先看下效果图,如果你感兴趣,非常欢迎你继续往下看
开始之前还是有必要再啰嗦几句的:
1、伴随着iOS10的出现,意味着不久的将来,又有不少应用最低的适配版本要提升到iOS9了,最低也是iOS8了吧,这也就意味着UIWebview
已经要退出历史舞台,会被WKWebview
替代,本身前者就是有严重的内存问题的,而且如果是JS调用原生的方法,那么交互方式也是比较麻烦的(制定特殊协议头的URL,在对应代理方法中拦截URL,然后判断是否符合要求,最后还要进行字符串的截取等操作),将来会不会大规模使用SFSafariViewController
浏览网页也是看后面的技术更新速度啦;
2、现在是一个移动先行的时代,无论是从bug修复速度还是公司效益等方面来说一个APP里面没有网页是不能算好的,目前已经完全进入了原生与h5混编的时代,所以说一个iOS程序员已经不得不会一些前端的知识了,而且将来会不会大规模使用React Native
进行跨平台的开发都是说不准的,所以说你不会前端的知识,在以后你是没有任何竞争力的;
3、本文中是使用的苹果的API进行的JS与原生交互
,并不涉及第三方框架WebViewJavascriptBridge
,它的详细使用可以点击这里查看,另外WKWebview
的API精讲以及使用说明你可以点击这里进行查看文章1或文章2,在本文以下部分,我会认为你已经掌握了WKWebview
的基本使用,OK,下面就进入本文的主题了。
- 首先开始之前的准备工作就是了解网易新闻后台返回的数据格式,我们借助
Charles
拦截到它对应的请求,然后用浏览器打开,直接复制所有的内容,去JSON数据解析网站进行转换之后的数据格式就是这个样子的
/*下面就是网易新闻详情页返回内容的大致格式
{
"C1EIHLG905298O5P":{
"body":"<!--IMG#0--><p> 本文的........</p>",//文章的核心内容
"users":Array[0],
"img":[
{
"ref":"<!--IMG#0-->", // 图片的占位符
"pixel":"750*300", //图片的宽高
"alt":"", //图片的文字说明
"src":"http://dingyue.nosdn.127.net/Tr8iUc8j2n5PBgxr3omZKxNqu7IwGb2PzKGzhZ0b612fJ1474379598042compressflag.jpg"
}, //图片的路径
],
"title":"NBA两亿先生跑不出这三位 除了威少哈登还有谁?",//文章的标题
"ptime":"2016-09-20 21:56:57"//文章的发布时间
}
}
*/
1、数据格式也是一目了然,每一个新闻都有一个不同的标识,类似于
C1EIHLG905298O5P
这种的,通过这个标识我们就可以拿到新闻数据的字典了,然后通过对应的key就可以拿到对应的新闻标题,来源,时间等信息;
2、后台返回的数据格式也是非常的好的,相信大部分新闻公司也是这样做的,在body里面你可以看到这样的格式,没错这个就是技巧所在,这个你可以理解为一个图片占位符,`body`里面的内容都是需要显示到网页的,我们都知道网页里面的都是各种各样的标签,所以仅仅
肯定是不会进行展示的,那么我们看第三步;
3、技巧就是我们通过img
这个key拿到这篇文章的所有图片,也就是一个数组,然后遍历这个图片数组依次取出没个图片的占位符、文字说明、图片的路径,然后我们自己创建一个h5
的img
标签,把这些东西都包装到该标签里面,再把这个标签和图片的占位符进行字符串替换就可以了,这样我们加载网页的时候图片就会正常显示了;
4、里面几个注意点需要提前说一下
-
有的图片是有文字说明,有的是没有的,所以我们需要做容错判断
-
进行字符串替换的时候是有返回值的,一定要有一个对象接收替换后的字符串,不然替换不起作用
-
使用
WKWebview
加载网页的时候,如果有本地的CSS
以及JS
文件需要一同加载,我们使用loadHTMLString:baseURL:
的时候,baseURL
这个参数不能为空,我们需要传入程序的资源路径,确保Html
代码就和css
以及js
是一个路径的。不然WKWebview
是无法加载的,这样你的网页效果就不会起到作用了,在使用UIWebview
的时候你可以传空。 -
了解了数据格式以及以下注意点之后,我们就开始上代码了
-
首先你需要导入
#import <WebKit/WebKit.h>
-
其次,你需要遵守2个协议,
WKUIDelegate
和WKScriptMessageHandler
,前者是当你的JS代码里有弹框等事件的时候相关的,后者是当你的JS代码需要调用原生的方法的时候相关的 -
由于
WKWebview
已经提供了加载进度的属性estimatedProgress
,并且它是支持KVO的,因此我们就可以弄一个真实的进度条去显示网页的加载进度了,不像之前UIWebview
,为了好的用户体验,我们去弄一个假的加载进度
- (void)viewDidLoad {
[super viewDidLoad];
//创建一个WKWebView的配置对象
WKWebViewConfiguration *configur = [[WKWebViewConfiguration alloc] init];
//设置configur对象的preferences属性的信息
WKPreferences *preferences = [[WKPreferences alloc] init];
configur.preferences = preferences;
//是否允许与js进行交互,默认是YES的,如果设置为NO,js的代码就不起作用了
preferences.javaScriptEnabled = YES;
/*设置configur对象的WKUserContentController属性的信息,也就是设置js可与webview内容交互配置
1、通过这个对象可以注入js名称,在js端通过window.webkit.messageHandlers.自定义的js名称.postMessage(如果有参数可以传递参数)方法来发送消息到native;
2、我们需要遵守WKScriptMessageHandler协议,设置代理,然后实现对应代理方法(userContentController:didReceiveScriptMessage:);
3、在上述代理方法里面就可以拿到对应的参数以及原生的方法名,我们就可以通过NSSelectorFromString包装成一个SEL,然后performSelector调用就可以了
4、以上内容是WKWebview和UIWebview针对JS调用原生的方法最大的区别(UIWebview中主要是通过是否允许加载对应url的那个代理方法,通过在js代码里面写好特殊的url,然后拦截到对应的url,进行字符串的匹配以及截取操作,最后包装成SEL,然后调用就可以了)
*/
/*
上述是理论说明,结合下面的实际代码再做一次解释,保你一看就明白
1、通过addScriptMessageHandler:name:方法,我们就可以注入js名称了,其实这个名称最好就是跟你的方法名一样,这样方便你包装使用,我这里自己写的就是openBigPicture,对应js中的代码就是window.webkit.messageHandlers.openBigPicture.postMessage()
2、因为我的方法是有参数的,参数就是图片的url,因为点击网页中的图片,要调用原生的浏览大图的方法,所以你可以通过字符串拼接的方式给"openBigPicture"拼接成"openBigPicture:",我这里没有采用这种方式,我传递的参数直接是字典,字典里面放了方法名以及图片的url,到时候直接取出来用就可以了
3、我的js代码中关于这块的代码是
window.webkit.messageHandlers.openBigPicture.postMessage({methodName:"openBigPicture:",imageSrc:imageArray[this.index].src});
4、js和原生交互这块内容离不开
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message{}这个代理方法,这个方法以及参数说明请到下面方法对应处
*/
WKUserContentController *userContentController = [[WKUserContentController alloc]init];
[userContentController addScriptMessageHandler:self name:@"openBigPicture"];
[userContentController addScriptMessageHandler:self name:@"openVideoPlayer"];
configur.userContentController = userContentController;
WKWebView *wkWebView = [[WKWebView alloc] initWithFrame:CGRectMake(0, 0, ScreenWidth, ScreenHeight) configuration:configur];
//WKWebview的estimatedProgress属性,就是加载进度,它是支持KVO监听进度改变的
[wkWebView addObserver:self forKeyPath:@"estimatedProgress" options:NSKeyValueObservingOptionNew context:nil];
[self.contentView addSubview:wkWebView];
self.wkWebView = wkWebView;
self.automaticallyAdjustsScrollViewInsets = NO;
//设置内边距底部,主要是为了让网页最后的内容不被底部的toolBar挡着
wkWebView.scrollView.contentInset = UIEdgeInsetsMake(0, 0, 104, 0);
//这句代码是让竖直方向的滚动条显示在正确的位置
wkWebView.scrollView.scrollIndicatorInsets = wkWebView.scrollView.contentInset;
wkWebView.UIDelegate = self;
self.wkWebView.navigationDelegate = self;
//自定义的方法,发送网络请求,获取新闻数据
[self getContentHtml];
NSLog(@"%@",NSStringFromCGRect(self.wkWebView.frame));
}
//该方法作用就是创建一个网络请求任务去加载requst请求,然后把服务器返回的data数据进行反序列化处理,根据网易新闻返回的数据格式,实质就是一个字典
- (void)getContentHtml
{
NSURLSession *session = [NSURLSession sharedSession];
NSURLRequest *requst = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://c.m.163.com/nc/article/C1EIHLG905298O5P/full.html"]];
NSURLSessionDataTask *task = [session dataTaskWithRequest:requst completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
if (error == nil) {
NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:nil];
self.htmlDict = dict[@"C1EIHLG905298O5P"];
//回到主线程刷新UI
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
self.navigationItem.title= self.htmlDict[@"title"];
}];
[self loadingHtmlNews];
}
}];
//开启任务
[task resume];
}
//进度值的监听
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context
{
self.progressView.progress = self.wkWebView.estimatedProgress;
// NSLog(@"%f", self.progressView.progress);
//我这里之所以判断加载到大于60%,睡1s的原因是网速太快了,看不到进度条更新,所以停顿一下,主要是想明显看到进度条进度改变,没有其他意思的
if (self.progressView.progress >0.6) {
sleep(1);
}
//网页加载完毕隐藏进度条
self.progressView.hidden = (self.wkWebView.estimatedProgress >= 1.0);
}
- (void)dealloc
{
[self removeObserver:self forKeyPath:@"estimatedProgress"];
}
//加载网页的,也算是核心方法了,里面有设计拼接网页元素,以及h5的相关知识
- (void)loadingHtmlNews
{
//文章内容
NSString *body = self.htmlDict[@"body"];
//文章标题
NSString *title = self.htmlDict[@"title"];
//视频
NSDictionary *videoDict = self.htmlDict[@"video"][0];
NSString *videoUrl = videoDict[@"mp4_url"];
NSString *alt = videoDict[@"alt"];
NSString *videoRef = videoDict[@"ref"];
NSString *videoHtml = [NSString stringWithFormat:@"<div>\\
<video class=\\"video0\\" src=\\"%@\\" autoPlay=\\"true\\">\\
</video>\\
<div class=\\"videoDescribe\\">%@</div>\\
</div>\\
",videoUrl,alt];
if (videoRef) {
body = [body stringByReplacingOccurrencesOfString:videoRef withString:videoHtml];
}
//来源
//来源01--网易号
NSString *sourceName = [NSString string];
if(self.htmlDict[@"articleTags"]){
sourceName = self.htmlDict[@"articleTags"];
}else {
sourceName = self.htmlDict[@"source"];
}
//来源02--发布时间
NSString *sourceTime = self.htmlDict[@"ptime"];
//文章里面的图片
NSArray *imagArray = self.htmlDict[@"img"];
for (NSDictionary *imageDict in imagArray) {
//图片在body中的占位标识,比如"<!--IMG#3-->"
NSString *imageRef = imageDict[@"ref"];
//图片的url
NSString *imageSrc = imageDict[@"src"];
//图片下面的文字说明
NSString *imageAlt = imageDict[@"alt"];
NSString *imageHtml = [NSString string];
//把对应的图片url转换成html里面显示图片的代码
if (imageAlt) {
imageHtml = [NSString stringWithFormat:@"<div><img width=\\"100%%\\" src=\\"%@\\"><div class=\\"picDescribe\\">%@</div></div>",imageSrc,imageAlt];
}else{
imageHtml = [NSString stringWithFormat:@"<div><img width=\\"100%%\\" src=\\"%@\\"></div>",imageSrc];
}
//这一步是显示图片的关键,主要就是把body里面的图片的占位标识给替换成上一步已经生成的html语法格式的图片代码,这样WKWebview加载html之后图片就可以被加载显示出来了
body = [body stringByReplacingOccurrencesOfString:imageRef withString:imageHtml];
}
//css文件的全路径
NSURL *cssPath = [[NSBundle mainBundle] URLForResource:@"newDetail" withExtension:@"css"];
// NSURL *videoPath = [[NSBundle mainBundle] URLForResource:@"video-js" withExtension:@"css"];
//js文件的路径
NSURL *jsPath = [[NSBundle mainBundle] URLForResource:@"newDetail" withExtension:@"js"];
//这里就是把前面的数据融入到html代码里面了,关于html的语法知识这里就不多说了,如果有不明白的可以咨询我或者亲自去w3c网站学习的-----“http://www.w3school.com.cn/”
//OC中使用'\\'就相当于说明了‘\\’后面的内容和前面都是一起的
NSString *html = [NSString stringWithFormat:@"\\
<html lang=\\"en\\">\\
<head>\\
<meta charset=\\"UTF-8\\">\\
<link href=\\"%@\\" rel=\\"stylesheet\\">\\
<link rel=\\"stylesheet\\" href=\\"http://cdn.static.runoob.com/libs/bootstrap/3.3.7/css/bootstrap.min.css\\">\\
<script src=\\"%@\\"type=\\"text/javascript\\"></script>\\
</head>\\
<body id=\\"mainBody\\">\\
<header>\\
<div id=\\"father\\">\\
<div id=\\"mainTitle\\">%@</div>\\
<div id=\\"sourceTitle\\"><span class=\\"source\\">%@</span><span class=\\"time\\">%@</span></div>\\
<video id=\\"video1\\" autoPlay=\\"true\\" src=\\"http://flv2.bn.netease.com/videolib3/1609/24/VFTsu6784/HD/VFTsu6784-mobile.mp4\\" controls=\\"controls\\">\\
</video>\\
<div class=\\"button01 glyphicon glyphicon-play\\"></div>\\
<p class=\\"lindan\\">超级丹吊炸天</p>\\
<div>%@</div>\\
</div>\\
</header>\\
</body>\\
</html>"\\
,cssPath,jsPath,title,sourceName,sourceTime,body];
//NSLog(@"%@",html);
//这里需要说明一下,(loadHTMLString:baseURL:)这个方法的第二个参数,之前用UIWebview写的时候只需要传递nil即可正常加载本地css以及js文件,但是换成WKWebview之后你再传递nil,那么css以及js的代码就不会起任何作用,当时写的时候遇到了这个问题,谷歌了发现也有朋友遇到这个问题,但是还没有找到比较好的解决答案,后来自己又搜索了一下,从一个朋友的一句话中有了发现,就修改成了现在的正确代码,然后效果就可以正常显示了
//使用现在这种写法之后,baseURL就指向了程序的资源路径,这样Html代码就和css以及js是一个路径的。不然WKWebview是无法加载的。当然baseURL也可以写一个网络路径,这样就可以用网络上的CSS了
[self.wkWebView loadHTMLString:html baseURL:[NSURL fileURLWithPath:[[NSBundle mainBundle] resourcePath]]];
}
//下面是关于之前我们遵守的2个协议的一些代理方法了,是JS与原生交互的关键
#pragma mark - WKScriptMessageHandler
/*
1、js调用原生的方法就会走这个方法
2、message参数里面有2个参数我们比较有用,name和body,
2.1 :其中name就是之前已经通过addScriptMessageHandler:name:方法注入的js名称
2.2 :其中body就是我们传递的参数了,我在js端传入的是一个字典,所以取出来也是字典,字典里面包含原生方法名以及被点击图片的url
*/
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message
{
//NSLog(@"%@,%@",message.name,message.body);
NSDictionary *imageDict = message.body;
NSString *src = [NSString string];
if (imageDict[@"imageSrc"]) {
src = imageDict[@"imageSrc"];
}else{
src = imageDict[@"videoSrc"];
}
NSString *name = imageDict[@"methodName"];
//如果方法名是我们需要的,那么说明是时候调用原生对应的方法了
if ([picMethodName isEqualToString:name]) {
SEL sel = NSSelectorFromString(picMethodName);
#pragma clang diagnostic push
#pragma clang diagnostic ignored"-Warc-performSelector-leaks"
//写在这个中间的代码,都不会被编译器提示PerformSelector may cause a leak because its selector is unknown类型的警告
[self performSelector:sel withObject:src];
#pragma clang diagnostic pop
}else if ([videoMethodName isEqualToString:name]){
SEL sel = NSSelectorFromString(name);
#pragma clang diagnostic push
#pragma clang diagnostic ignored"-Warc-performSelector-leaks"
[self performSelector:sel withObject:src];
#pragma clang diagnostic pop
}
}
#pragma mark - WKUIDelegate(js弹框需要实现的代理方法)
//使用了WKWebView后,在JS端调用alert()是不会在HTML中显式弹出窗口,是我们需要在该方法中手动弹出iOS系统的alert的
//该方法中的message参数就是我们JS代码中alert函数里面的参数内容
- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler
{
// NSLog(@"js弹框了");
UIAlertController *alertView = [UIAlertController alertControllerWithTitle:@"JS-Coder" message:message preferredStyle:UIAlertControllerStyleAlert];
[alertView addAction:[UIAlertAction actionWithTitle:@"Sure" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
//一定要调用下这个block
//API说明:The completion handler to call after the alert panel has been dismissed
completionHandler();
}]];
[self presentViewController:alertView animated:YES completion:nil];
}
//自己写的关于图片浏览以及视频播放的方法
#pragma mark - JS调用 OC的方法进行图片浏览
- (void)openBigPicture:(NSString *)imageSrc
{
//NSLog(@"%@",imageSrc);
LBBigPictureViewController *picVc = [[LBBigPictureViewController alloc] init];
picVc.modalTransitionStyle = UIModalTransitionStylePartialCurl;
picVc.imageSrc = imageSrc;
[self presentViewController:picVc animated:YES completion:nil];
}
#pragma mark - JS调用 OC的方法进行视频播放
- (void)openVideoPlayer:(NSString *)videoSrc
{
LBVideoPlayerController *videoPlayer = [[LBVideoPlayerController alloc] init];
videoPlayer.videoSrc = videoSrc;
[self presentViewController:videoPlayer animated:YES completion:nil];
}
- 最后,JS的代码你可以看下这里,CSS文件就不放上来了,有兴趣的可以去下载代码看一下哦
window.onload = function(){
alert('感谢你的支持');
var imageArray = document.getElementsByTagName("img");
for(var i=0; i < imageArray.length; i++)
{
var image = imageArray[i];
image.index = i;
image.onclick = function(){
// alert(imageArray[this.index].src);
//向外面传递了一个字典出去,里面有需要调用的方法名以及点击的图片对应的路径,这样外界拿到这个字典就可以随意操作了 window.webkit.messageHandlers.openBigPicture.postMessage({methodName:"openBigPicture:",imageSrc:imageArray[this.index].src});
}
}