WKWebView
Mattt Thompson撰写、 Croath Liu翻译、 发布于2014年8月24日
iOS 与 web 之间的关系非常复杂,这种复杂关系甚至可以追溯到几十年前系统建立初期。
其实现在很难说清第一代 iPhone 横空出世是一件多么困难的事情。我们现今司空见惯的触摸屏在当时只是诸多方案中的一种。最早期的产品原型是物理键盘、触摸屏、触控笔的结合,屏幕尺寸才是 5" x 7"。甚至当时 iPod 的轮子都是一个严肃的备选方案。
但最最重要的决定或许都是由软件而非硬件决定的。
iPhone 应该如何运行软件呢?像 OS X 上的应用程序,或者 web 页面,以及 Safari 都应该如何运行起来呢?仿照 OS X 去构建 iPhone OS 的方法已经广泛地被大家熟知了,这种方法到今天也留下了不少争议。
我们来回忆一下 Steve Jobs 2007 年 WWDC keynote 上这句臭名昭著的话:
iPhone 包含了整个 Safari 引擎。因此,你可以创作应用 Ajax 技术的 Web 2.0 应用,表现上和使用上都和 iPhone 原生应用一模一样。这些应用可以和 iPhone 的各种服务完美地集成到一起:这些应用可以有打电话的功能,可以发邮件,可以在 Google Maps 上寻找地标。
Web 一直是 iOS 系统上的二级公民(讽刺的是,其实现今移动网页响应式设计的出现大多是 iPhone 推动的)。UIWebView
笨重难用,还有内存泄漏,和 Nirtro JavaScript 引擎谈笑风生的 Safari 不知道要比它高到哪里去了。
然而,这所有的一切都会因为 WKWebView
和 WebKit
框架其他部分的出现而发生改变。
WKWebView
是现代 WebKit API 在 iOS 8 和 OS X Yosemite 应用中的核心部分。它代替了 UIKit 中的 UIWebView
和 AppKit 中的 WebView
,提供了统一的跨双平台 API。
自诩拥有 60fps 滚动刷新率、内置手势、高效的 app 和 web 信息交换通道、和 Safari 相同的 JavaScript 引擎,WKWebView
毫无疑问地成为了 WWDC 2014 上的最亮点。
UIWebView
& UIWebViewDelegate
这个两个东西是如何在 WKWebKit 中被重构成 14 个类 3 个协议的呢。虽然这次的变化确实带来了不少的新功能,但请一定不要因此感到恐慌!
WKWebKit Framework
Classes
WKBackForwardList
: 之前访问过的 web 页面的列表,可以通过后退和前进动作来访问到。
WKBackForwardListItem
: webview 中后退列表里的某一个网页。WKFrameInfo
: 包含一个网页的布局信息。WKNavigation
: 包含一个网页的加载进度信息。
WKNavigationAction
: 包含可能让网页导航变化的信息,用于判断是否做出导航变化。WKNavigationResponse
: 包含可能让网页导航变化的返回内容信息,用于判断是否做出导航变化。WKPreferences
: 概括一个 webview 的偏好设置。WKProcessPool
: 表示一个 web 内容加载池。WKUserContentController
: 提供使用 JavaScript post 信息和注射 script 的方法。
WKScriptMessage
: 包含网页发出的信息。WKUserScript
: 表示可以被网页接受的用户脚本。 > -WKWebViewConfiguration
: 初始化 webview 的设置。WKWindowFeatures
: 指定加载新网页时的窗口属性。
Protocols
WKNavigationDelegate
: 提供了追踪主窗口网页加载过程和判断主窗口和子窗口是否进行页面加载新页面的相关方法。WKScriptMessageHandler
: 提供从网页中收消息的回调方法。WKUIDelegate
: 提供用原生控件显示网页的方法回调。
UIWebView
和 WKWebView
的 API 区别
WKWebView
继承了 UIWebView
大部分的接口,这让 app 来继承 WKWebKit 也简单了许多(同时随着更新 iOS 8 的越来越多这也成为了某种必需)。
有兴趣的同学可以看一下这两个类的 API 区别:
UIWebView | WKWebView |
---|---|
var scrollView: UIScrollView! { get } | var scrollView: UIScrollView! { get } |
var configuration: WKWebViewConfiguration! { get } | |
var delegate: UIWebViewDelegate! | var UIDelegate: WKUIDelegate! |
var navigationDelegate: WKNavigationDelegate! | |
var backForwardList: WKBackForwardList! { get } |
页面加载
UIWebView | WKWebView |
---|---|
func loadRequest(request: NSURLRequest!) | func loadRequest(request: NSURLRequest!) -> WKNavigation! |
func loadHTMLString(string: String!, baseURL: NSURL!) | func loadHTMLString(string: String!, baseURL: NSURL!) -> WKNavigation! |
func loadData(data: NSData!, MIMEType: String!, textEncodingName: String!, baseURL: NSURL!) | |
var estimatedProgress: Double { get } | |
var hasOnlySecureContent: Bool { get } | |
func reload() | func reload() -> WKNavigation! |
func reloadFromOrigin() -> WKNavigation! | |
func stopLoading() | func stopLoading() |
var request: NSURLRequest! { get } | |
var URL: NSURL! { get } | |
var title: String! { get } |
访问历史
UIWebView | WKWebView |
---|---|
func goToBackForwardListItem(item: WKBackForwardListItem!) -> WKNavigation! | |
func goBack() | func goBack() -> WKNavigation! |
func goForward() | func goForward() -> WKNavigation! |
var canGoBack: Bool { get } | var canGoBack: Bool { get } |
var canGoForward: Bool { get } | var canGoForward: Bool { get } |
var loading: Bool { get } | var loading: Bool { get } |
调用 Javascript
UIWebView | WKWebView |
---|---|
func stringByEvaluatingJavaScriptFromString(script: String!) -> String! | |
func evaluateJavaScript(javaScriptString: String!, completionHandler: ((AnyObject!, NSError!) -> Void)!) |
原生混用
UIWebView | WKWebView |
---|---|
var keyboardDisplayRequiresUserAction: Bool | |
var scalesPageToFit: Bool | |
var allowsBackForwardNavigationGestures: Bool |
页码
WKWebView
目前缺少关于页码相关的 API。
var paginationMode: UIWebPaginationMode
var paginationBreakingMode: UIWebPaginationBreakingMode
var pageLength: CGFloat
var gapBetweenPages: CGFloat
var pageCount: Int { get }
重构分离开的 WKWebViewConfiguration
下面这些 UIWebView
的属性被重构进了在初始化 WKWebView
传入的设置对象:
var allowsInlineMediaPlayback: Bool
var mediaPlaybackRequiresUserAction: Bool
var mediaPlaybackAllowsAirPlay: Bool
var suppressesIncrementalRendering: Bool
JavaScript ↔︎ Swift 对话机制
相对于 UIWebView
最大的提升就是数据在可以 app 和 web 内容之间传递。
使用用户脚本来注入 JavaScript
WKUserScript
允许在正文加载之前或之后注入到页面中。这个强大的功能允许在页面中以安全且唯一的方式操作网页内容。
一个简单的例子如下,用户改变背景的用户脚本被插入到网页中:
let source = "document.body.style.background = \"#777\";"
let userScript = WKUserScript(source: source, injectionTime: .AtDocumentEnd, forMainFrameOnly: true)
let userContentController = WKUserContentController()
userContentController.addUserScript(userScript)
let configuration = WKWebViewConfiguration()
configuration.userContentController = userContentController
self.webView = WKWebView(frame: self.view.bounds, configuration: configuration)
WKUserScript
对象可以以 JavaScript 源码形式初始化,初始化时还可以传入是在加载之前还是结束时注入,以及脚本影响的是这个布局还是仅主要布局。于是用户脚本被加入到 WKUserContentController
中,并且以 WKWebViewConfiguration
属性传入到 WKWebView
的初始化过程中。
这个样例可以简单扩展为更为高级的页面修改方法,例如去除广告、隐藏评论等,更复杂的样例见此:让所有出现的"the cloud"变为"my butt"。
马杀鸡 魂斗罗(Message Handlers 抱歉我憋了一天,真的不会翻译)
web 和 app 通讯机制也通过 message handler 有很大提升。
就想在Safari 审查元素功能中的 console.log
能在调试终端打印信息一样,网页中的信息也可以通过调用这个函数被传到 app 里:
window.webkit.messageHandlers.{NAME}.postMessage()
这个 API 真正神奇的地方在于 JavaScript 对象可以自动转换为 Objective-C 或 Swift 对象。
Handler 的名字可以通过 WKScriptMessageHandler
协议中的 addScriptMessageHandler()
接口函数设置:
class NotificationScriptMessageHandler: NSObject, WKScriptMessageHandler {
func userContentController(userContentController: WKUserContentController, didReceiveScriptMessage message: WKScriptMessage!) {
println(message.body)
}
}
let userContentController = WKUserContentController()
let handler = NotificationScriptMessageHandler()
userContentController.addScriptMessageHandler(handler, name: "notification")
于是当通知进入 app 的时候,比如说在页面中创建一个新对象,相关信息就可以这样传递:
window.webkit.messageHandlers.notification.postMessage({body: "..."});
添加用户脚本来对 web 事件监听并用 Message Handler 将信息传回 app。
同样的方法也可以用来收集页面信息用于 app 的页面展示或数据分析。
例如,如果某人要针对 NSHipster.com 做一个特别的浏览器,就可以加一个能够呼出相似文章列表的按钮:
// document.location.href == "http://nshipster.com/webkit"
function getRelatedArticles() {
var related = [];
var elements = document.getElementById("related").getElementsByTagName("a");
for (i = 0; i < elements.length; i++) {
var a = elements[i];
related.push({href: a.href, title: a.title});
}
window.webkit.messageHandlers.related.postMessage({articles: related});
}
如果你的 app 只是对网页内容做了很简单的一层包装,那么 WKWebView
可以彻底改变这种状况。你对于性能和兼容性的所有愿望都将变为现实。所有你想要的东西都在这了。
1、使用UIWebView加载网页
运行XCode 4.3,新建一个Single View Application,命名为WebViewDemo。
2、加载WebView
在ViewController.h添加WebView成员变量和在ViewController.m添加实现
- #import <UIKit/UIKit.h>
- @interface ViewController : UIViewController
- {
- UIWebView *webView;
- }
- @end
- ViewController.m
- - (void)viewDidLoad
- {
- [super viewDidLoad];
- webView = [[UIWebView alloc] initWithFrame:CGRectMake(0, 0, 320, 480)];
- NSURLRequest *request =[NSURLRequest requestWithURL:[NSURL URLWithString:@"http://www.baidu.com"]];
- [self.view addSubview: webView];
- [webView loadRequest:request];
- }
手机的网络环境是实时变化的,网络慢的时候,怎么提示用户网页正在打开呢?在网页打开出错的时候怎么提示用户呢?这时候我们就需要知道网页什么时候打开的,
什么时候加载完成,什么时候出错了。那么我们需要实现这个<UIWebViewDelegate>协议
3、实现协议,在ViewController.h修改如下:
- #import <UIKit/UIKit.h>
- @interface ViewController : UIViewController<UIWebViewDelegate>
- {
- UIWebView *webView;
- }
- @end
UIWebView中几个重要的函数
1.- (void )webViewDidStartLoad:(UIWebView *)webView 网页开始加载的时候调用
2.- (void )webViewDidFinishLoad:(UIWebView *)webView 网页加载完成的时候调用
3.- (void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error 网页加载错误的时候调用
4、实现这三个方法,加入NSLog。
先在viewDidLoad 的webView实例化下面加上
[webView setDelegate:self];设置代理。这样上面的三个方法才能得到回调。
三个方法实现如下:
- <span style="font-family:Arial, Verdana, sans-serif;color:#333333;">- (void) webViewDidStartLoad:(UIWebView *)webView
- {
- NSLog(@"webViewDidStartLoad");
- }
- - (void) webViewDidFinishLoad:(UIWebView *)webView
- {
- NSLog(@"webViewDidFinishLoad");
- }
- - (void) webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error
- {
- NSLog(@"didFailLoadWithError:%@", error);
- }
- </span>
2012-06-23 15:20:29.728 WebViewDemo[1001:f803] webViewDidStartLoad
2012-06-23 15:20:29.991 WebViewDemo[1001:f803] webViewDidFinishLoad
那我们试试error情况,把wifi关掉,运行打印结果:2012-06-23 15:23:58.939 WebViewDemo[1087:f803] webViewDidStartLoad
2012-06-23 15:23:59.016 WebViewDemo[1087:f803] webViewDidFinishLoad
请求结果不变,为什么关掉网络还成功了呢?缓存?我换163.com试试,这是真正的结果出来了:
2012-06-23 15:24:41.131 WebViewDemo[1134:f803] webViewDidStartLoad
2012-06-23 15:24:41.149 WebViewDemo[1134:f803] didFailLoadWithError:Error Domain=NSURLErrorDomain Code=-1009 "The Internet connection appears to be offline." UserInfo=0x6b41660 {NSErrorFailingURLStringKey=http://www.163.com/, NSErrorFailingURLKey=http://www.163.com/, NSLocalizedDescription=The Internet connection appears to be offline., NSUnderlyingError=0x6eae690 "The Internet connection appears to be offline."}
连接错误了,调用了 didFailLoadWithError。5、加载等待界面
为了给用户更直观的界面效果,我们加上等待的loading界面试试
在webViewDidStartLoad加入等待
- <strong>- (void) webViewDidStartLoad:(UIWebView *)webView
- {
- //创建UIActivityIndicatorView背底半透明View
- UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 320, 480)];
- [view setTag:108];
- [view setBackgroundColor:[UIColor blackColor]];
- [view setAlpha:0.5];
- [self.view addSubview:view];
- activityIndicator = [[UIActivityIndicatorView alloc] initWithFrame:CGRectMake(0.0f, 0.0f, 32.0f, 32.0f)];
- [activityIndicator setCenter:view.center];
- [activityIndicator setActivityIndicatorViewStyle:UIActivityIndicatorViewStyleWhite];
- [view addSubview:activityIndicator];
- [activityIndicator startAnimating];
- </strong>
加载完成或失败时,去掉loading效果
- <strong>- (void) webViewDidFinishLoad:(UIWebView *)webView
- {
- [activityIndicator stopAnimating];
- UIView *view = (UIView*)[self.view viewWithTag:108];
- [view removeFromSuperview];
- NSLog(@"webViewDidFinishLoad");
- }
- - (void) webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error
- {
- [activityIndicator stopAnimating];
- UIView *view = (UIView*)[self.view viewWithTag:108];
- [view removeFromSuperview];
- </strong>
// ViewController.m
// UIWebView
//
// Created by xalo on 15/10/13.
// Copyright © 2015 年 蓝鸥科技 . All rights reserved.
//
#import "ViewController.h"
@interface ViewController ()< UIWebViewDelegate >
@end
@implementation ViewController
- ( void )viewDidLoad {
[ super viewDidLoad ];
// Do any additional setup after loading the view, typically from a nib.
UIWebView *web = [[ UIWebView alloc ] initWithFrame : self . view . bounds ];
// 数据源
// - (void)loadRequest:(NSURLRequest *)request;
// - (void)loadHTMLString:(NSString *)string baseURL:(nullable NSURL *)baseURL;
// - (void)loadData:(NSData *)data MIMEType:(NSString *)MIMEType textEncodingName:(NSString *)textEncodingName baseURL:(NSURL *)baseURL;
// 第一种 :URL
NSURLRequest *request = [ NSURLRequest requestWithURL :[ NSURL URLWithString : @"http://www.sina.com.cn" ]];
[ self . view addSubview :web];
[web loadRequest :request];
// 第二种 :HTML
// NSString *HTMLData = @"<img src=\"test2.png\" />ddd";
// [web loadHTMLString:HTMLData baseURL:[NSURL fileURLWithPath: [[NSBundle mainBundle] bundlePath]]];
// 第三种 :data
// NSString *path = [[NSBundle mainBundle] pathForResource:@" 关于 .txt" ofType:nil];
// NSURL *url = [NSURL fileURLWithPath:path];
// NSData *data = [NSData dataWithContentsOfFile:path];
// [web loadData:data MIMEType:@"text/plain" textEncodingName:@"UTF-8" baseURL:nil];
web. delegate = self ;
}
// UIWebViewDelegate 中几个重要的函数
// 1.- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
// 2.- (void )webViewDidStartLoad:(UIWebView *)webView 网页开始加载的时候调用
// 3.- (void )webViewDidFinishLoad:(UIWebView *)webView 网页加载完成的时候调用
// 4.- (void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error 网页加载错误的时候调用
#pragma mark - <UIWebViewDelegate>
- ( BOOL )webView:( UIWebView *)webView shouldStartLoadWithRequest:( NSURLRequest *)request navigationType:( UIWebViewNavigationType )navigationType{
NSLog ( @" 将要开始加载请求 " );
return YES ;
}
- ( void )webViewDidStartLoad:( UIWebView *)webView{
NSLog ( @" 开始加载 " );
// 实现等待界面
// 加载背景视图
UIView *view = [[ UIView alloc ] initWithFrame : self . view . bounds ];
view. tag = 108 ;
view. backgroundColor = [ UIColor blackColor ];
view. alpha = 0.3 ;
[ self . view addSubview :view];
// 设置加载动画
UIActivityIndicatorView *activityIndicator = [[ UIActivityIndicatorView alloc ] initWithFrame : CGRectMake ( 0 , 0 , 30 , 30 )];
activityIndicator. tag = 109 ;
activityIndicator. center = self . view . center ;
[activityIndicator setActivityIndicatorViewStyle : UIActivityIndicatorViewStyleWhite ]; // 加载标识样式有三种
[view addSubview :activityIndicator];
[activityIndicator startAnimating ]; // 开始动画
}
//webView 结束加载
- ( void )webViewDidFinishLoad:( UIWebView *)webView{
NSLog ( @" 结束加载 " );
UIActivityIndicatorView *activity = ( UIActivityIndicatorView *)[ self . view viewWithTag : 109 ];
[activity stopAnimating ]; // 动画停止
UIView *view = ( UIView *)[ self . view viewWithTag : 108 ];
[view removeFromSuperview ]; // 移除遮盖视图
}
//webView 加载失败
- ( void )webView:( UIWebView *)webView didFailLoadWithError:( NSError *)error{
NSLog ( @" 加载失败 %@" , error);
UIActivityIndicatorView *activity = ( UIActivityIndicatorView *)[ self . view viewWithTag : 109 ];
[activity stopAnimating ]; // 动画停止
UIView *view = ( UIView *)[ self . view viewWithTag : 108 ];
[view removeFromSuperview ]; // 移除遮盖视图
}
- ( void )didReceiveMemoryWarning {
[ super didReceiveMemoryWarning ];
// Dispose of any resources that can be recreated.
}