在第一部分 (中译版)中,我们了解了WebKit框架的基础部分。在本篇文章中,我们会深入了解WebKit框架并学习如何在原生App中定制网页。我们也会学习如何从网页中获得数据,并在App中使用数据。
接下来我们将建立一个专门浏览appcoda.com的App。首先,请下载初始项目。初始项目就是一个名为Coda的简单浏览器,跟我们在第一个部分编写的App差不多。唯一的区别就是没有textfield控件给用户输入url,而且我也将前进、后退和刷新按钮更换成了图片。
处理外部链接
如果你运行这个App并点击了一个外部链接,webview会加载这个链接。但是这个App使用来浏览Appcoda的,所以我们需要防止加载外部链接。如果用户点击了外部链接,这个链接的内容就会在Safari中打开。
我们需要的是定制网页加载的方式。达到这个目标,我们需要干涉加载网页的正常过程。在完成这个目标之前,让我们先来了解一下网页加载的过程。
网页加载由一个动作(Action)触发。这可能是任何导致网页加载的动作,比如:触碰一个链接、点击后退、前进和刷新按钮,JavaScript 设置了window.location属性,子窗口的加载或者对WKWebView的loadRequest()方法的调用。然后一个请求被发送到了服务 器,我们会得到一个响应(可能是有意义的也可能是错误状态码,比如:404)。最后服务器会发送更多地数据,并结束加载过程。
WebKit允许你的App在动作(Action)和响应(Response)阶段之间注入代码,并决定是否继续加载,取消或是做你想做的事情。
在ViewController中加入如下方法。
1
2
3
4
5
6
7
8
|
func webView(webView: WKWebView!, decidePolicyForNavigationAction navigationAction: WKNavigationAction!, decisionHandler: ((WKNavigationActionPolicy) -> Void)!) {
if
(navigationAction.navigationType == WKNavigationType.LinkActivated && !navigationAction.request.URL.host!.lowercaseString.hasPrefix(
"www.appcoda.com"
)) {
UIApplication.sharedApplication().openURL(navigationAction.request.URL)
decisionHandler(WKNavigationActionPolicy.Cancel)
}
else
{
decisionHandler(WKNavigationActionPolicy.Allow)
}
}
|
上述是一个WKNavigationDelegate代理方法,在网页加载时会被多次调用。其中一个参数WKNavigationAction对象 包含了帮助你决定是否让一个网页被加载的信息。在上面的代码中,我们使用其中两个属性,navigationType和request。我们只想中断被用户点击的外部链接的加载过程,所以我们检查了navigationType。然后我们检查了request的url来确认它是否是一个外部链接。如果两个条件都满足,这个url就会在用户的浏览器中打开(通常都是Safari)并且WKNavigationActionPolicy.Cancel终止了 App加载网页的过程。否则这个网页就会被加载并显示。
运行这个程序,点击任何外部链接,这个链接都会在Safari中被加载。
设置网页标题
如果网页能有标题来提示用户在哪里的话,这将会非常有用。在前面的文章中,我们学习了一些WKWebView的KVO属性比如loading和estimatedProgress。title也是一个KVO属性,我们将用它来获得当前网页的标题。
在viewDidLoad()其他addObserver()方法下面加入如下代码:
1
|
webView.addObserver(self, forKeyPath:
"title"
, options: .New, context: nil)
|
然后在observeValueForKeyPath(_:, ofObject:)方法其他if语句下方加入如下代码。
1
2
3
|
if
(keyPath ==
"title"
) {
title = webView.title
}
|
运行程序,随便逛逛,你将发现navigationbar的title会被正确地更新。
修改网页内容
现在这个Coda App是一个Appcoda的专用浏览器,但是我们还可以做几样事情来提升一下用户体验。
因为设备的特性,移动App以简明的方式展示数据和信息。用户希望能看到他们想看的东西,而且不用做大量的滑动来获得信息。
目前为止,这个App展示了Appcoda网页的所有内容。我们想忽略某些和网页内容相关程度不大的东西。我们将会移除侧边栏和底部展示《Appcoda Swift book》的栏目。
为了达到这个目标,我们使用JavaScript向网页注入CSS规则以隐藏这些栏目。首先,我们需要检查网页然后决定规则。
为了检查网页,我们使用大多数浏览器都支持的开发者工具。你也可以自己以插件(plugins)或者add-ons的形式安装到你的浏览器,比如火狐的Firebug。我将使用Chrome的开发者工具,但你可以使用任何你喜欢的。其过程大致一样。
打开Chrome开发者工具,View->Developer->Developer Tools。
这将在浏览器底部打开一个开发者窗口。开发者窗口将和上半部分左边的网页源码和邮编的CSS样式查看分离开来。在底部,是JavaScript命令行,这里你可以输入你的代码,它将会在网页执行。
我们需要检查id属性然后标记处我们想要隐藏的栏目。
侧边栏会在所有的网页中显示,而底部的书籍栏目只会在文章页面显示。点击任意一篇文章,打开开发者工具。首先,右击侧边栏并选择检查元素。在开发者 窗口中,会高亮显示对应的元素代码。如果你将你的鼠标移动到对应的代码,网页部分相对应的区域也会高亮显示。我们希望得到包含了整个侧边栏的根元素 id(或者class)。
根据你选择审查元素时所处的位置,向上折叠标签直到只有侧边栏在页面中被高亮显示。上一个被折叠的标签就是我们要的根元素。在这里,他是一个div标签,id为’sidebar‘。
在将代码写入你的App之前,你最好在浏览器中测试一下它。因为如果发生了什么错误的话,在App中调试会非常困难。我们首先在浏览器中测试CSS和JavaScript。
点击我们在上面找到的div标签。在窗口的右边你将会看见它的CSS布局。点击+按钮添加一条CSS规则,如下所示。
1
2
|
div
#sidebar {
}
|
在上面的代码中添加如下代码:
1
|
display:none;
|
在你添加完上述代码后,侧边栏应该会从页面消失。
现在删除这条布局规则以显示侧边栏。现在我们要使用JavaScript往DOM中添加代码。在html页面之下,是运行JavaScript的命令行。将如下代码粘贴到命令行。
1
|
var
styleTag = document.createElement(
"style"
);
|
上述代码创建了一个元素并赋值给了一个变量。接下来我们如下代码,他将会给这个元素添加css规则。我也把底部书籍栏目也添加了进去。
1
|
styleTag.textContent =
'div#sidebar, .after-post.widget-area {display:none;}'
;
|
最后,使用下面的代码给DOM添加样式标签。这段代码会马上执行,侧边栏和底部的书籍栏目会消失。
1
|
document.documentElement.appendChild(styleTag);
|
上面的几个过程是隐藏页面元素的必须过程。
回到Xcode,创建一个新文件File->New->File->iOS->Other->Empty并命名为hideSection.js。添加如下代码。
1
2
3
|
var
styleTag = document.createElement(
"style"
);
styleTag.textContent =
'div#sidebar, .after-post.widget-area {display:none;}'
;
document.documentElement.appendChild(styleTag);
|
在ViewController中,替换init()中方法为如下:
1
2
3
4
5
6
7
8
9
10
|
required init(coder aDecoder: NSCoder) {
let config = WKWebViewConfiguration()
let scriptURL = NSBundle.mainBundle().pathForResource(
"hideSections"
, ofType:
"js"
)
let scriptContent = String(contentsOfFile:scriptURL!, encoding:NSUTF8StringEncoding, error: nil)
let script = WKUserScript(source: scriptContent!, injectionTime: .AtDocumentStart, forMainFrameOnly:
true
)
config.userContentController.addUserScript(script)
self.webView = WKWebView(frame: CGRectZero, configuration: config)
super
.init(coder: aDecoder)
self.webView.navigationDelegate = self
}
|
上述代码创建了一个WKWebViewConfiguration对象,它拥有一些属性来作为原生代码和网页之间沟通的桥梁。JavaScript 代码被一个WKUserScript对象加载和包装。然后这个脚本被赋值给WKWebViewConfiguration对象的 userContentController属性,接着webView使用这个配置来初始化。
当创建WKUserScript对象时,我们决定这个脚本什么时候应该被注入,和被作用于整个页面或者某个特定的frame。
运行程序,你不在会看到侧边栏(在iPhone中,它将会在页面底部以下的区域显示)和底部书籍栏目了。
提取网页数据
Appcoda的主页显示最近的10篇文章。当我们在设备上浏览主页时,你必须滑动许多次以看到底部的内容。我们希望有一个更简单地方式来获取最近的文章。我们将创建一个tableview来保存最近的文章。
我们通过提取网页数据来创建这个tableview。这里我不再会注入html了。我将会给出一段我所使用的用来获得文章的JavaScript代码,并解释它如何工作。
如果你在主页运行如下JavaScript代码,一列包含这些文章的标题和url的数据将会被打印到命令行。
1
2
3
4
5
6
7
8
9
|
var
postsWrapper = document.querySelector(
'#content'
)
var
posts = postsWrapper.querySelectorAll(
'.post.type-post.status-publish'
)
for
(
var
i = 0; i < posts.length; i++) {
var
post = posts[i];
var
postTitle = post.querySelector(
'h2.entry-title a'
).textContent;
var
postURL = post.querySelector(
'h2.entry-title a'
).getAttribute(
'href'
);
console.log(
"Title: "
, postTitle,
" URL: "
, postURL);
}
|
如果你观察网页文章部分的html结构,你回发现类似下面的东西。
在上面的JavaScript代码中,我们通过‘content’id获得元素。这个是一个div元素,文章列表的中间父元素。我们将会获得这个 div下的所有子元素,然后赋值给posts变量。它将会持有一个class为post的div数组。我们遍历这个数组,获得每个h2标签中得得文本。我 们也通过另外一个链接标记的href属性来获得每个文章的URL。然后我们打印这些内容。
打开Xcode,创建一个新文件File->New->File->iOS->Other->Empth。命名为getPost.js。粘贴如下代码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
var
postsWrapper = document.querySelector(
'#content'
)
var
posts = postsWrapper.querySelectorAll(
'.post.type-post.status-publish'
)
function
parsePosts() {
pos = []
for
(
var
i = 0; i < posts.length; i++) {
var
post = posts[i];
var
postTitle = post.querySelector(
'h2.entry-title a'
).textContent;
var
postURL = post.querySelector(
'h2.entry-title a'
).getAttribute(
'href'
);
pos.push({
'postTitle'
: postTitle,
'postURL'
: postURL});
}
return
pos
}
var
postsList = parsePosts();
webkit.messageHandlers.didGetPosts.postMessage(postsList);
|
上面的代码获得了所有文章的标题和url并把他们保存到了一个数组。最后一行代码是的JavaScript和原生代码之间能够交流。 webkit.messageHandlers是一个全局对象,用来帮助触发原生代码回调。didGetPosts代表了和一个原生代码方法一样名字的消 息。postMessage向回调中传递了postsList数组。
在故事板中,拖放一个导航栏按钮到导航栏的左边。并改变它的名称为‘Recent’。然后创建一个它的outlet并命名为recentPostsButton。你应该会看到如下代码。
1
|
@IBOutlet weak
var
recentPostsButton: UIBarButtonItem!
|
在viewDidLoad()方法底部,添加如下代码。我们希望这个按钮一直不可点,直到posts数组有了数据。
1
|
recentPostsButton.enabled =
false
|
在ViewController,import语句下面添加如下代码。
1
|
let MessageHandler =
"didGetPosts"
|
在类文件中添加如下属性。
1
|
var
postsWebView: WKWebView?
|
在viewDidLoad()底部添加如下代码。
1
2
3
4
5
6
7
8
|
let config = WKWebViewConfiguration()
let scriptURL = NSBundle.mainBundle().pathForResource(
"getPosts"
, ofType:
"js"
)
let scriptContent = String(contentsOfFile:scriptURL!, encoding:NSUTF8StringEncoding, error: nil)
let script = WKUserScript(source: scriptContent!, injectionTime: .AtDocumentEnd, forMainFrameOnly:
true
)
config.userContentController.addUserScript(script)
config.userContentController.addScriptMessageHandler(self, name: MessageHandler)
postsWebView = WKWebView(frame: CGRectZero, configuration: config)
|
这里我们像之前一样导入一个JavaScript文件,我们只希望DOM被构建好时及.AtDocumentEnd时被注入一次。我们也将MessageHandler加入了WKWebViewConfiguration作为WKWebView初始化的配置。
更新类声明,遵循WKScriptMessageHandler协议。
1
|
class ViewController: UIViewController, WKNavigationDelegate, WKScriptMessageHandler
|
我们建立一个模型(model)文件来保存文章数据。创建一个文件File->New->File->iOS->Source->Cocoa Touch Class。命名为Post并作为NSObject的子类。在类中粘贴如下代码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
import UIKit
class Post: NSObject {
var
postTitle: String =
""
var
postURL: String =
""
init(dictionary: Dictionary) {
self.postTitle = dictionary[
"postTitle"
]!
self.postURL = dictionary[
"postURL"
]!
super
.init()
}
}
|
在ViewController类中添加如下变量。
1
|
var
posts: [Post] = []
|
添加WKScripMessageHandler协议必须遵守的方法。
1
2
3
4
5
6
7
8
9
10
11
|
func userContentController(userContentController: WKUserContentController, didReceiveScriptMessage message: WKScriptMessage) {
if
(message.name == MessageHandler) {
if
let postsList = message.body as? [Dictionary] {
for
ps
in
postsList {
let post = Post(dictionary: ps)
posts.append(post)
}
recentPostsButton.enabled =
true
}
}
}
|
上面的代码首先将检查接收到得消息是否是我们想要的,如果是,就会将消息中的数据提取成一个字典数组,然后使用其中的字典创建Post对象,并将这些Post对象依次添加到posts数组中,最后recentPostsButton就可被点击了。
打开故事版,在画板中添加一个Table View Controller。选择它,使用Editor->Embed In->Navigation Controller将它嵌入一个navigation controller。
按下Control,点击View Controller中得Recent按钮,拖到这个新的navigation controller中,选择popover presentation from the popup。选择这个被新创建了segue,设置它的Identifier为‘recentPosts’。
创建一个新文件File->New->File->iOS->Source->Cocoa Touch class。命名为PostsTableViewController并选择为UITableViewController的子类。
在故事板中,选择创建的Table View Controller,选择 Identity Inspector,设置class为PostsTableViewController。选择table view的prototype cell,在Attributes Inspector中设置Identifier为postCell。
向PostTableViewController添加如下代码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
import UIKit
class PostsTableViewController: UITableViewController {
var
posts: [Post] = []
override init(style: UITableViewStyle) {
super
.init(style: style)
}
required init(coder aDecoder: NSCoder) {
super
.init(coder: aDecoder)
}
override func viewDidLoad() {
super
.viewDidLoad()
self.title =
"Recent Articles"
tableView.reloadData()
}
override func numberOfSectionsInTableView(tableView:
UITableView?) -> Int {
return
1
}
override func tableView(tableView: UITableView?, numberOfRowsInSection section: Int) -> Int {
return
posts.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier(
"postCell"
, forIndexPath: indexPath) as UITableViewCell
let post = posts[indexPath.row]
cell.textLabel?.text = post.postTitle
return
cell
}
}
|
这里我们实现了tableview的数据源,他将会显示文章的标题。
添加下面代码到ViewController。
1
2
3
4
5
6
7
|
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject!) {
if
(segue.identifier ==
"recentPosts"
) {
let navigationController = segue.destinationViewController as UINavigationController
let postsViewController = navigationController.topViewController as PostsTableViewController
postsViewController.posts = posts
}
}
|
当点击Recent按钮时,此方法会被调用。在显示tableview View controller之前,它将posts数组传递给了tableview view controller。
运行程序。点击Recent按钮,你会看到一个充满了文章列表的tableview。在iPhone上,它已满屏显示,在iPad在一个popover中显示。
当你点击一个cell的时候,没有任何事情发生。我们希望被点击的文章被加载到web view上面。
在ViewController中,添加如下代码到import语句下面。
1
|
let PostSelected =
"postSelected"
|
当点击一个cell的时候,我们将发送一个通知。上面的常量就是这个通知的名字。
在PostsTableViewController中添加如下方法。
1
2
3
4
5
|
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
let post = posts[indexPath.row]
NSNotificationCenter.defaultCenter().postNotificationName(PostSelected, object: post)
dismissViewControllerAnimated(
true
, completion: nil)
}
|
在上述方法中,每当一个cell被点击的时候会发送一个通知并隐藏(dismiss)这个tableview controller。
在ViewController类中,viewDidLoad()方法底部添加如下代码。
1
|
NSNotificationCenter.defaultCenter().addObserver(self, selector:
"postSelected:"
, name: PostSelected, object: nil)
|
上述代码将这个ViewController设置为了cell点击发送的通知的观察者(observer)。
在ViewController中添加如下方法。
1
2
3
4
5
|
func postSelected(notification:NSNotification) {
webView.loadRequest(NSURLRequest())
let post = notification.object as Post
webView.loadRequest(NSURLRequest(URL:NSURL(string:post.postURL)!))
}
|
通过上面的方法我们得到了通知中附加的post,然后加载了post中的url。
运行程序,你应该可以在tableview中的任意文章之间切换了。
到目前为止,当我们点击Recent按钮时,我们无法隐藏(dismissing)tableview,除非我们选择并点击一篇文章。我们需要添加一个取消按钮。
在故事版中,在Table view controller的导航栏(navigationbar)的右边添加一个按钮。设置它的Identifier为Cancel。
打开Assistan Editor,按下Control点击Cancel按钮拖动到PostTableViewController类中创建一个方法。命名为cancel,确保其参数类型为UIBarButtonItem。按照下面的代码编辑这个方法。
1
2
3
|
@IBAction func cancel(sender: UIBarButtonItem) {
dismissViewControllerAnimated(
true
, completion: nil)
}
|
现在你应该有一个取消按钮了,它可以用来隐藏这个Table view。
结论
新的WebKit框架使得开发者能够让App和网页内容之间实现无缝交互。我们学习了如何自定义网页样式。从网页中提取数据,并在App中使用这些数据。
如果你的App只是一个网页版App的容器,使用WebKit框架吧!它将带来如原生App般的性能和操作体验。WebKit框架将会为这些体验不好的App力挽狂澜。
如果你想了解更多关于此框架的内容,这个WWDC视频将是个非常好的开始。
你可以在这里下载完整项目。