WKWebView终极指南

语言: swift, 版本:4.2,XCode:10.2
写作时间:2019-04-23

WKWebView

WKWebView在iOS 8 以后提供的高性能的Web容器. 本文列举15个最常用的用法。

1. 创建一个WKWebView覆盖全屏

在viewDidLoad() 中创建WKWebView,并增加为view的subView或者直接替换掉view. 这样做的弊端是,后续想要用到WKWebView就比较困难.

一种简单的方式是增加一个WKWebView的属性:

let webView = WKWebView()

在 loadView() 的方法去设置ViewController‘s view:

override func loadView() {
    self.view = webView
}

拥有一个WKWebView的属性,可以方便后面引用webview的属性和方法。

2. 加载远程内容URL

WKWebView最重要的功能是加载一个远程内容的url, 它比较古怪不是一句代码调用. 反而,先用string创建一个URL对象, 接着封装为URLRequest对象, 最后通知WKWebView去加载URLRequest对象:

func loadRemoteUrl(_ urlString: String) {
    if let url = URL(string: urlString) {
        let request = URLRequest(url: url)
        webView.load(request)
    }
}

如果比较频繁调用远程URLs,可以给WKWebView封装一个extension,使加载URL变成一句话方法:

extension WKWebView {
    func load(_ urlString: String) {
        if let url = URL(string: urlString) {
            let request = URLRequest(url: url)
            load(request)
        }
    }
}

现在可以一句话加载远程内容URL

webView.load("https://www.apple.com").

3. 加载本地内容

WKWebView可以用 loadFileURL() 加载已经预置在bundle里面的HTML. 你可以提供File URL指向bundle中HTML文件.

举个?, 如果你要加载一个文件叫 “help.html” :

func loadLocalFile() {
    if let url = Bundle.main.url(forResource: "help", withExtension: "html") {
        let path = url.deletingLastPathComponent()
        print("url: \(url)")
        print("path: \(path)")
        webView.loadFileURL(url, allowingReadAccessTo: path)

    }
}

help.html , 这里help.css的路径为相对路径,因为Bundle下面的路径都会被打包到根目录下,详情请看下面的文件截图。

<html>
  <head>
    <link href="help.css" rel="stylesheet" />
  head>
  <body>
  <h1>Hi, do you need help?h1>
  <a href="https://www.apple.com">apple.coma>
  <div id="username">@zgpeacediv>
  phone:<span class="nowrap">18686868866span>
  body>
    
html>

url.deletingLastPathComponent()告诉WebKit它可以读取在目录下,help.html引用的文件,比如images, JavaScript, CSS.

help.css

h1 {
    margin-top:200px;
    color:red;
}

参数allowingReadAccessTo 用url, 或者url.deletingLastPathComponent(),都能加载出css的样式。难道是目录如果是同一个文件夹都可以用这两种方式来用?知情者帮忙指正,谢谢。

WKWebView终极指南_第1张图片
运行效果如下:
WKWebView终极指南_第2张图片

4. 4. 加载HTML片段字符串

WKWebView可以直接加载HTML代码字符串:

func loadHtmlFragments() {
        let html = """
        <html>
            <head>
                <link href="help.css" rel="stylesheet" />
            </head>
            <body>
                <h1>Hello, Swift!</h1>
            </body>
        </html>
        """
        
//        webView.loadHTMLString(html, baseURL: nil)
        webView.loadHTMLString(html, baseURL: Bundle.main.resourceURL)
    }

方法webView.loadHTMLString(html, baseURL: nil)中注意参数baseURL. 如果你引用相关资源比如 images, 或者 CSS, 你可以指定 Bundle.main.resourceURL:

webView.loadHTMLString(html, baseURL: Bundle.main.resourceURL)

5. 5. 控制哪些网站可以访问

WKWebView默认允许访问所有网站, 当然这个权限是可以配置的.
首先,需要把类遵循代理协议 WKNavigationDelegate – 它可以是view controller, 也可以是其它类. 比如:

class ViewController: UIViewController, WKNavigationDelegate {

接着, 设置webView的navigation delegate为当前类:

webView.navigationDelegate = self

最后, 实现方法decidePolicyFor, 增加逻辑决定哪些页面可以加载。作为例子, 下面的实现允许访问host为www.apple.com的网站:

func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
    if let host = navigationAction.request.url?.host {
        if host == "www.apple.com" {
            decisionHandler(.allow)
            return
        }
    }

    decisionHandler(.cancel)
}

6. 通过浏览器打开链接

通常,有些链接在App内部打开,有些链接通过外部浏览器打开。实现这个功能不负责,归功于WKNavigationDelegate protocol.

首先, 标志类遵循代理协议WKNavigationDelegate:

class ViewController: UIViewController, WKNavigationDelegate {

接着, 设置webView的navigation delegate为当前类:

webView.navigationDelegate = self

最后, 实现代理方法decidePolicyFor,表明哪些逻辑用内部App还是外部浏览器打开. 外部浏览器打开用方法UIApplication.shared.open() ,接着需要断开App内部打开的逻辑,用方法decisionHandler(),参数用.cancel .

举个?, 这里的实现host为www.apple.com的网站用外部浏览器打开:

func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
    if let url = navigationAction.request.url {
        if url.host == "www.apple.com" {
            UIApplication.shared.open(url)
            decisionHandler(.cancel)
            return
        }
    }

    decisionHandler(.allow)
}

7. 监控页面打开进度

加载web页面流程包括,抓取相关HTML, 下载用到JavaScript,CSS, images, 等.

为了提升用户体验,需要告诉用户加载的进度. 实现这需要观察observing属性estimatedProgress :

webView.addObserver(self, forKeyPath: #keyPath(WKWebView.estimatedProgress), options: .new, context: nil)

增加观察者属性后,接着实现通知方法observeValue(forKeyPath:) , 它可以通知你一个字符串改变的内容. 如果是属性“estimatedProgress”的改变,可以通过进度条的方式告诉用户,这里只是 打印进度信息:

override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
    if keyPath == "estimatedProgress" {
        print(Float(webView.estimatedProgress))
    }
}

8. 读取网页的标题,当它改变的时候

你可以用属性webView.title去读取当前页面的标题, 因为页面的标题会跟着用户浏览不同的页面而改变,实时修改页面的标题可以提升用户体验.

实现这个,首先注册观察的属性:

webView.addObserver(self, forKeyPath: #keyPath(WKWebView.title), options: .new, context: nil)

最后实现通知的方法observeValue(forKeyPath:) method. 这里会传递给你更新的标题,可以更新导航栏信息.

这里只是打印更新的标题:

override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
    if keyPath == "title" {
        if let title = webView.title {
            print(title)
        }
    }
}

9. 读取访问过的历史页面

浏览器可以前进和后退,就是因为这些痕迹都被记录. 这些都存储在webView的属性backForwardList中, 它包含了两个数组backList, forwardList.

在这两个数组中,你可以访问所有前进和后退的页面,一般会用到标题和链接url:

func printHistoryBackList() {
    for page in webView.backForwardList.backList {
        print("User visited title:\(page.title), url: \(page.url.absoluteString)")
    }
}

10. 注射JavaScript到页面中

一旦webView加载内容,你可以调用方法evaluateJavaScript()去执行任何JavaScript到已渲染完毕的页面. 你仅仅需要执行JavaScript – 读取网页上的内容, 比如 – 当JavaScript运行后,下面的闭包执行.

举个?, 你的页面包含内容

@twostraws
and you wanted to read the “@zgpeace” , 你可以通过下面代码获取:

func injectJavaScriptIntoAPage() {
        webView.evaluateJavaScript("document.getElementById('username').innerText") { (result, error) in
        if let result = result {
            print(result)
        }
    }
}

11. 读取和删除cookies

可以通过webView属性httpCookieStore读取cookies列表. 这是藏在configuration.websiteDataStore的属性, 一旦你找到它可以调用方法getAllCookies()获取cookies数组, 或者调用方法delete()去删除指定的cookie.

举个?, 下面代码遍历所有cookies, 如果找到名字叫“authentication”则删掉它 – 其它的cookies则打印出来:

func manageCookies() {
        webView.configuration.websiteDataStore.httpCookieStore.getAllCookies { (cookies) in
        for cookie in cookies {
            if cookie.name == "authentication" {
                self.webView.configuration.websiteDataStore.httpCookieStore.delete(cookie, completionHandler: nil)
            } else {
                print("\(cookie.name) is set to \(cookie.value)")
            }
        }
    }
}

12. 设置user agent

User agents可以告诉服务器端web servers,去鉴别客户端浏览器就是App里面的,常用于访问一些受限制的资源.

如果你从服务端读取页面,你可以设置user agent,使服务器端可以鉴别是你的App:

func customUserAgent() {
	print("old agent: \(String(describing: webView.customUserAgent))")
	webView.customUserAgent = "My Awesome App"
	print("new agent: \(String(describing: webView.customUserAgent))")
}

注意: 你可以修改user agent,当你去访问其它资源的时候。但是记住,有些网站可能会困惑,浏览器怎么会是没见过的名字.

13. 显示定制的UI

WKWebView就像只有一个长条tab的iOS Safari app, 说明用户不能打开或者关闭一个新的windows去浏览多个页面, 并且它不能由JavaScript触发后显示Alert或者Confirmation确认请求弹框.

幸运的是,你可以实现上面的诉求通过代理协议WKUIDelegate: 设置webView的UI delegate, 你就可以显示定制的alerts, 管理你的tabs, 等.

首先,设置类遵循协议WKUIDelegate:

class ViewController: UIViewController, WKUIDelegate {

接着,指派webView的uiDelegate属性为当前对象:

webView.uiDelegate = self

最后,实现WKUIDelegate的方法.
举个?,通过WKWebView显示alert controller, 当html页面用JavaScript的方法alert():

func webView(_ webView: WKWebView, runJavaScriptAlertPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping () -> Void) {
    let ac = UIAlertController(title: "Hey, listen!", message: message, preferredStyle: .alert)
    ac.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
    present(ac, animated: true)
    completionHandler()
}

还有其它代理方法,比如runJavaScriptConfirmPanelWithMessage显示确定和取消按钮的UI, runJavaScriptTextInputPanelWithPrompt 显示text input控件,索取用户反馈, 等等.

注意: 在执行结束后,你必须调用completion handler. JavaScript’s alerts会被阻塞, 这就意味着JavaScript execution不会显示Alert页面直到completion handler 结束. 结果是, WebKit会投诉如果你不告诉它已经处理完毕,告诉的方法就是completionHandler().

测试代码

func showingAlertUI() {
    webView.evaluateJavaScript("alert('boom');", completionHandler: nil)
}

14. 快照页面的一部分(也可以全部)

虽然你可以用方法drawHierarchy(),把view转换为image, WebKit可以用方法takeSnapshot()剪切任意大小的image.

举个?,可以从左上角开始剪切150x50 image, 并把剪切的页面当做subView显示于view上面:

func snapshotPartOfThePage() {
   let config = WKSnapshotConfiguration()
    config.rect = CGRect(x: 0, y: 200, width: 150, height: 50)
    
    webView.takeSnapshot(with: config) { (image, error) in
        if let image = image {
            print(image.size)
            let imageView = UIImageView(image: image)
            self.view.addSubview(imageView)
        }
    }
}

如果你要截屏整个view – 把cofnig改为nil即可.

15. 检测数据

WebViews已经支持数据类型检测, 比如电话号码 phone numbers, 日历事件calendar events, 航班号码flight numbers都可以编程可点击的链接 tappable links.

上面数据类型默认是普通文本, 但是可以被覆盖 – 当你创建webView时候 ,定制参数对象WKWebViewConfiguration.

举个?, 这里命令webView去检测所有可能地数据类型:

let config = WKWebViewConfiguration()
config.dataDetectorTypes = [.all]
let webView = WKWebView(frame: .zero, configuration: config)

结果截图,可以对比第3小节的截图
WKWebView终极指南_第3张图片

16. webview中遇到的问题

_blank 问题:

a便签中如果有_blank属性,表示在新的窗口打开页面,但是我们打不开,解决办法是添加如下代理的实现,直接在当前窗口加载url

-(WKWebView )webView:(WKWebView )webView createWebViewWithConfiguration:(WKWebViewConfiguration )configuration forNavigationAction:(WKNavigationAction )navigationAction windowFeatures:(WKWindowFeatures *)windowFeatures
{

if (!navigationAction.targetFrame.isMainFrame) {
    [webView loadRequest:navigationAction.request];
}
return nil;
}

cookie问题、白屏问题

参考 WKWebView 那些坑解决的,都有不错的解决方法

17. WKWebview代理

WKWebView有两个代理,WKUIDelegate 和 WKNavigationDelegate。WKNavigationDelegate主要处理一些跳转、加载处理操作;WKUIDelegate主要处理UI相关的操作:确认框,警告框,提示框。因此WKNavigationDelegate更加常用。下面是WKNavigationDelegate的代理列表:

///二、页面是否加载 ,相当于shouldStartLoadWithRequest

- (void) webView: (WKWebView *) webView decidePolicyForNavigationAction: (WKNavigationAction*) navigationAction decisionHandler: (void (^)(WKNavigationActionPolicy)) decisionHandler
{
    //允许跳转
    decisionHandler(WKNavigationResponsePolicyAllow);
    //不允许跳转
    //decisionHandler(WKNavigationResponsePolicyCancel);
}

///三、页面开始加载

- (void)webView:(WKWebView*)webView didStartProvisionalNavigation:(WKNavigation*)navigation
{

}

///四、收到响应

- (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler 
{
    //允许跳转
    decisionHandler(WKNavigationResponsePolicyAllow);
    //不允许跳转
    //decisionHandler(WKNavigationResponsePolicyCancel);
}

///五、收到数据

- (void)webView:(WKWebView *)webView didCommitNavigation:(WKNavigation *)navigation 
{ 
}

///六、页面加载完成

- (void)webView:(WKWebView*)webView didFinishNavigation:(WKNavigation*)navigation
{
}

///七、页面加载失败

- (void)webView:(WKWebView*)theWebView didFailNavigation:(WKNavigation*)navigation withError:(NSError*)error
{

}

/// 八、跳转失败

- (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(null_unspecified WKNavigation *)navigation withError:(NSError *)error
{

}

/// 九、重定向

- (void)webView:(WKWebView *)webView didReceiveServerRedirectForProvisionalNavigation:(null_unspecified WKNavigation *)navigation
{
}

///十、这个目的解决白屏问题 (ios9以上)

- (void)webViewWebContentProcessDidTerminate:(WKWebView *)webView API_AVAILABLE(macosx(10.11), ios(9.0))
{
    [self loadRequestWithUrl:self.strCurrentURL];
}

WKUIDelegate有下面三个代理:

- (void)                        webView:(WKWebView*)webView
     runJavaScriptAlertPanelWithMessage:(NSString*)message
                       initiatedByFrame:(WKFrameInfo*)frame
                      completionHandler:(void (^)(void))completionHandler
                      
- (void)                        webView:(WKWebView*)webView
   runJavaScriptConfirmPanelWithMessage:(NSString*)message
                       initiatedByFrame:(WKFrameInfo*)frame
                      completionHandler:(void (^)(BOOL result))completionHandler
                      
- (void)                        webView:(WKWebView*)webView
  runJavaScriptTextInputPanelWithPrompt:(NSString*)prompt
                            defaultText:(NSString*)defaultText
                       initiatedByFrame:(WKFrameInfo*)frame
                      completionHandler:(void (^)(NSString* result))completionHandler
                      

代码下载

https://github.com/zgpeace/WKWebViewGuide

参考

https://www.hackingwithswift.com/articles/112/the-ultimate-guide-to-wkwebview
http://www.cnblogs.com/NSong/p/6489802.html
https://jianli2017.top/wiki/Hybird/webview/gome-Webview/

你可能感兴趣的:(iOS)