谈谈iOS9中的WebKit 与 Safari
每个用过 UIWebView
的iOS开发者对其诸多的限制和有限的功能也深有感触。悻然,自iOS8推出 WebKit 框架后将改变这一窘境。在本文我将会深入WebKit来体验一下它给我们带来的好处,同时也看看在iOS9中新加入的 SFSafariViewController 有些什么新的惊喜。
通用的浏览行为
所谓的通用浏览行为主要可以归纳为以下的几种:
- 网页载入进度
- 前进
- 后退
- 刷新
如果每个用到 WebView 的 app都要做一个专用的Controller也挺麻烦的,我以前就直接采用其它第三方写好的包来完成。
但现在,如果用 WKWebView 将变得很方便,以代码说话吧:
class ViewController: UIViewController {
var webView: WKWebView!
@IBOutlet weak var progressView: UIProgressView!
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)!
// 实例化 WKWebView
self.webView = WKWebView(frame: CGRectZero)
}
override func viewDidLoad() {
super.viewDidLoad()
// 编程式加入 WKWebView
view.addSubview(webView)
view.insertSubview(webView, aboveSubview: progressView)
webView.translatesAutoresizingMaskIntoConstraints = false
let widthConstraint = NSLayoutConstraint(item:webView, attribute: .Width, relatedBy: .Equal, toItem: view, attribute: .Width, multiplier: 1 , constant: 0)
view.addConstraint(widthConstraint)
let heightConstraint = NSLayoutConstraint(item:webView,attribute: .Height, relatedBy: .Equal,toItem: view, attribute: .Height, multiplier:1, constant: -46)
view.addConstraint(heightConstraint)
// 检测webView对象属性的变化
webView.addObserver(self, forKeyPath: "loading", options: .New, context: nil)
webView.addObserver(self, forKeyPath: "title", options: .New, context: nil)
//加载网页
let request = NSURLRequest(URL: NSURL(string: "http://ray.dotnetage.com")!)
webView.loadRequest(request)
}
override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {
if (keyPath == "loading") {
// 检测按钮的可用性
forwardButton.enabled = webView.canGoBack
backButton.enabled = webView.canGoBack
stopButton.image = webView.loading ? UIImage(name: "Cross") : UIImage(named: "Syncing")
} else if keyPath == "title" {
title = webView.title
} else if keyPath == "estimatedProgress" {
progressView.hidden = webView.estimatedProgress == 1
progressView.setProgress(Float(webView.estimatedProgress), animated: true)
}
}
}
这些代码我觉得没什么好说的,除了WKWebView不能通过 IB 来可视化构建外,以上的代码最多是将 Autolayout 部分的代码优化一下就是了。写一写,做个 Example 就懂了。
与 Javascript 通信
通过WebKit就不需要通过 javascript 桥的方式来与DOM通信了。其实这也不是什么新技术,早再 windows98 在VB或者在Delphi中也可以通过COM接口用完全相类似的手法与DOM通信了。
废话不多说,讲讲 WebKit 的基本原理吧。以下是 WebKit Host 的Web进程 与 App 主进程的通信关系示意图:
这里包含两个过程
执行 javascript 脚本
我们可以将 javascript 脚本包含于 App 的 Bundle 内,作为应用程序资源。在运行期将其通过 WebKit 注入至目标网页内执行。
首先我们要准备一个目标网页,这里就以我自己的博客来做一个示例(http://ray.dotnetage.com)。在 App 中用WebKit打开是这样的
现在,我就将我博客上首页的大标题的文字改掉,具体的代码很简单:
$(".page-header h1").text("iOS注入测试");
然后,在 iOS项目内增加一个叫 inject.js
的脚本文件,将上述代码复制其内。
在 App 内包含的 javascript 脚本最好先在浏览器的控制台内执行一次,以确保脚本自身是可以被正确执行的。如果脚本中含有潜在错误,在App内是无法检测得到的。
然后,在控制器的构造函数内创建一个 WKWebViewConfiguration
实例,并作为参数传入 WKWebView
的构造函数,具体代码如下:
// ViewController.swift
import WebKit
class ViewController : UIViewController {
var webView: WKWebView!
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)!
let configuation = WKWebViewConfiguration()
configuation.userContentController.addUserScript(getUserScript("inject"))
self.webView = WKWebView(frame: CGRectZero,configuration: configuation)
}
// 从资源中读取 javascript 脚本
func getUserScript(fromName: String)-> WKUserScript {
let filePath = NSBundle.mainBundle().pathForResource(fromName, ofType: "js")
let js = try! String(contentsOfFile: filePath!, encoding: NSUTF8StringEncoding)
return WKUserScript(source: js, injectionTime: .AtDocumentEnd, forMainFrameOnly: true)
}
...
}
此代码段中需要注意的另一点是在自定义方法 getUserScript()
所返回的 WKUserScript
对象。我们可以通过 injectionTime
决定将脚本注入至HTML的开始部分还是在文档的尾部。
再次执行代码,效果如下:
也就是说我们可以在 app 内通过 WebKit 注入javascript后就可以任意地操控页面内的所有 DOM 对象!
javascript 的回调
除了从 app 一端将代码注入到浏览器,执行一个动作。某些情况下我们还需要从网页上做某一些处理后,例如将网页内的某些元素读出并转为一个 json 对象集合,回传给 App 处理。又或者我们的 app 在加载一个网页之后想一次性地读出页面内的所有图像,当用户点击这些图像的时候我们用 app 的本地方式来全屏预览,诸如此类。在这些语境下:
我们都得从网页内返回对象
也就是说,在网页的进程内要向 app 进程通信,那么我们就需要在脚本中使用:
webkit.messageHandlers.{MessageName}.postMessage([params]);
这个方法在标准的HTML5浏览器是不能直接执行的,例如 Chrome和 Safair。只有通过 WebKit Host 的页面才会出现这个 webkit
对象。 这并不难理解,只是 WebKit 在加载页面后向 windows 注入了 webkit
这个实例,使得 javascript 可以通过它来向 app 发送信息。
如果我们要向 app 发送一个信息,例如:在页面上的一个按钮被点击后,执行 app 内打开相册的代码,那么就得先在 javascript 上写好这样的代码:
$("#mybutton").click(function(){
webkit.messageHandlers.openPhotoLibrary.postMessage();
});
请留意 openPhotoLibrary
这个对象在Swift是没有,当这个方法被回传到 Swift 的时候这只是一个消息的名字,而在Swift中要接收这种来至于浏览器发送的信息我们的控制器就需要实现 WKScriptMessageHandler
这个接口,它只有一个方法,我们多花些篇幅直接将这个接口的代码打开:
/*! A class conforming to the WKScriptMessageHandler protocol provides a
method for receiving messages from JavaScript running in a webpage.
*/
public protocol WKScriptMessageHandler : NSObjectProtocol {
/*! @abstract Invoked when a script message is received from a webpage.
@param userContentController The user content controller invoking the
delegate method.
@param message The script message received.
*/
@available(iOS 8.0, *)
public func userContentController(userContentController: WKUserContentController, didReceiveScriptMessage message: WKScriptMessage)
}
那么,我们就直接实现这个接口:
class ViewController: UIViewController, WKScriptMessageHandler {
required init(coder aDecoder: NSCoder) {
// ... 之前的代码同上
configuation.userContentController.addScriptMessageHandler(self, name: "openPhotoLibrary")
self.webView = WKWebView(frame: CGRectZero,configuration: configuation)
}
...
func userContentController(userContentController: WKUserContentController, didReceiveScriptMessage message: WKScriptMessage) {
if message.name == "openPhotoLibrary" {
// 这里就可以加入打开相册的代码了
}
}
}
从代码就可以看出原理的一二:
- 在构造
WKWebView
之前要用addScriptMessageHandler
方法向配置对象注册一个消息名,这里的例程是 "openPhotoLibrary"。 - 实现
WKScriptMessageHandler
接口,从userContentController()
方法的message.name
参数中判断消息的源头,执行对应的代码。
另外,如果我们需要从javascript脚本中向 app 传入对象,可以直接在 postMessage()
方法内将对象作为参数输入,但通常这个参数的类型应该是一个数组或者是普通的JSON对象,这样在 app 才能用字典对象将其从新读出。
例如,我从当前网页中将所有的菜单的地址和名称读出,并生成了一个 menus
的 javascript 数组对象:
var menus = $(".navbar a").map(function(n,i){
return {
title: $(n).text,
link: $(n).attr("href")
};
});
webkit.messageHandlers.didFetchMenus.postMessage(menus);
在 Swift 中定义一个 Menu
类:
class Menu {
var title = ""
var link = ""
init(dict : Dictionary<String,String>) {
self.title = dict["title"]!
self.link = dict["link"]!
}
}
这里就略过接口实现,直接看 userContentController
方法实现:
var menus: [Menus]?
func userContentController(userContentController: WKUserContentController, didReceiveScriptMessage message: WKScriptMessage) {
if message.name == "didFetchMenus" {
if let resultArray = message.body as? [Dictionary<String,String>] {
menus = resultArray.map{ Menu(dict: $0) }
// 这里就取出并将JSON转换为 Swift 的Menu对象了
print(menus)
}
}
}
iOS9 中的 Safair 浏览器
在 iOS9 中加入了 SafariServices
这个新的模块,其作用就是提供了一个全功能的内嵌式 Safair,通过
SFSafariViewController
就能像普通的 控制器那样使用。
以下是一个简单的例子
import UIKit
import SafariServices
class ViewController: UIViewController {
@IBAction func openBrowser(sender: AnyObject) {
let safari = SFSafariViewController(URL:NSURL(string:"http://www.apple.com")!)
self.showViewController(safari, sender: self)
}
}
SFSafariViewController
和 WebKit 的最大区别是 SFSafariViewController
没有什么可控制方法,只是一个可以完全嵌入到 app 中的一个控制器,避免了像以前那样如果打开一个外部链接要跳出当前的app,而且 SFSafariViewController
嵌入的 Safari 和 Safari 内的所有功能是一样的,同样支持 3D Touch 和切页的等特色功能。且当我们的 app 采用外部网络帐号进行集成登录时,Safari 能更直接获取到当前 app的应用上下文,而无须再跳出重新在外部登入后再返回至App。这无疑是大大地增强了 app 在与 Safari 集成的时的使用体验。
在 Apple 的开发者网站上对 WebKit 与 SafariServices 的选择上给出了这样的意见:
- 如果需要与网页交互则选择 WebKit
- 如果需要与Safari具有同样的使用体验且不需要与网页交互推荐使用 SafariServices
这确实是一项很不错的更新。
谈谈iOS9中的WebKit 与 Safari的更多相关文章
- 谈谈CSS中一些比较"偏门"的小知识
前面我写了:谈谈html中一些比较"偏门"的知识,现在这篇(主要)想谈谈个人所见的CSS一些小知识点,加深印象:同时也希望有需要的人能有收获! 1.常见的浏览器内核: 以IE为代表 ...
- 谈谈WCF中的Data Contract(3):WCF Data Contract对Collection & Dictionary的支持
谈谈WCF中的Data Contract(3):WCF Data Contract对Collection & Dictionary的支持 在本篇文章上一部分Order Processing的例 ...
- 谈谈项目中遇到的各种iOS7适配问题
由于我的项目要适配到iOS7.1, 而现在已经是9时代了,在实际工作中我也是遇到了各种奇葩的坑,所以我想尽快把遇到的iOS7适配问题和解决方案分享出来,以后这些东西可能就用处不大了. 1.字体问题 i ...
- ios9中 UIStackView的使用
ios9中 UIStackView的使用 by 伍雪颖 UIStackView能够垂直或水平排布多个subview, 自己主动为每一个subview创建和加入Auto Layout constrain ...
- 谈谈iOS中粘性动画以及果冻效果的实现
在最近做个一个自定义PageControl——KYAnimatedPageControl中,我实现了CALayer的形变动画以及CALayer的弹性动画,效果先过目: https://github.c ...
- iOS9中请求出现App Transport Security has blocked a cleartext HTTP (http://)
错误描述: App Transport Security has blocked a cleartext HTTP (http://) resource load since it is insecu ...
- 关于iOS9中的App Transport Security相关说明及适配(转)
原文:http://my.oschina.net/vimfung/blog/494687 iOS9中新增App Transport Security(简称ATS)特性, 主要使到原来请求的时候用到的H ...
- IOS9中联系人对象的使用及增删改查操作的函数封装
之前克服重重困难把IOS9中新的类联系人搞明白了,现在把增删改查封装成了函数,如下: // // ViewController.m // IOS9中联系人CNContact的使用 // // Crea ...
- iOS9中如何注册远程通知
大熊猫猪·侯佩原创或翻译作品.欢迎转载,转载请注明出处. 如果觉得写的不好请多提意见,如果觉得不错请多多支持点赞.谢谢! hopy ;) 在以往的版本中,我们可以通过: [[UIApplication ...
随机推荐
- 读书笔记——网络编程与开发技术(3)基于TCP/IP协议的网络编程相关知识
TCP/IP协议:数据链路层,网络层,传输层,应用层. IP地址分为5类:A类.B类.C类.D类.E类. (A类.B类.C类是基本类,D类多用于多播传送,E类为保留类.) "*"表 ...
- Ubuntu15.04装机配置脚本
#!/bin/bash echo "vim" sudo apt-get install vim cp -r ./vim/.vim ~/ cp ./vim/.vimrc ~/ ech ...
- 工作中常用的Linux命令:目录
工作两三年,每天都和Linux打交道,但每每使用Linux命令的时候却会像提笔忘字般不知如何使用,常常查手册或到网上找资料.此系列文章主要是为了方便自己在使用命令时随时可查阅.鄙人才疏学浅,文中若有任 ...
- Heartbeat+LVS构建高可用负载均衡集群
1.heartbeat简介: Heartbeat 项目是 Linux-HA 工程的一个组成部分,它实现了一个高可用集群系统.心跳服务和集群通信是高可用集群的两个关键组件,在 Heartbeat 项目里 ...
- volatile与synchronized关键字
volatile关键字相信了解Java多线程的读者都很清楚它的作用.volatile关键字用于声明简单类型变量,如int.float.boolean等数据类型.如果这些简单数据类型声明为volatil ...
- 【NOIP提高组2015D2T1】uva 714 copying books【二分答案】——yhx
Before the invention of book-printing, it was very hard to make a copy of a book. All the contents h ...
- MIT jos 6.828 Fall 2014 训练记录(lab 1)
注: 源代码参见我的github:https://github.com/YaoZengzeng/jos Part 1: PC Bootstrap +------------------+ <- ...
- opencv 3.0 DPM Cascade 检测 (附带TBB和openMP加速)
opencv 3.0 DPM cascade contrib模块 转载请注明出处,楼燚(yì)航的blog,http://www.cnblogs.com/louyihang-loves-baiyan/ ...
- Rem实现自适应初体验
第一次做移动端的页面,遇到的第一个问题就是移动端的轮播图.其实轮播图的插件有很多,但是完全满足需求的并不容易找. 需求: 1.实现基本的触屏轮播图效果 2.传入非标准比例的图片,可以自动平铺(有时候图 ...
- HDU 4122 Alice's mooncake shop --RMQ
题意: 一个月饼店做月饼,总营业时间m小时,只能在整点做月饼,可以做无限个,不过在不同的时间做月饼的话每个月饼的花费是不一样的,假设即为cost[i],再给n个订单,即为在某个时间要多少个月饼,时间从 ...