(原文:A Look at the WebKit Framework in iOS 8 – Part 2 作者:Joyce Echessa 譯者:ibenjamin )
在第一部分 (中譯版)中,我們了解了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中加入如下方法。
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()方法下面加入如下代碼:
webView.addObserver(self, forKeyPath: "title", options: .New, context: nil)
然後在observeValueForKeyPath(_:, ofObject:)方法其他if語句下方加入如下代碼。
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規則,如下所示。
div#sidebar { }
在上面的代碼中添加如下代碼:
display:none;
在你添加完上述代碼後,側邊欄應該會從頁面消失。
現在刪除這條布局規則以顯示側邊欄。現在我們要使用JavaScript往DOM中添加代碼。在html頁面之下,是運行JavaScript的命令行。將如下代碼粘貼到命令行。
var styleTag = document.createElement("style");
上述代碼創建了一個元素並賦值給了一個變量。接下來我們如下代碼,他將會給這個元素添加css規則。我也把底部書籍欄目也添加了進去。
styleTag.textContent = 'div#sidebar, .after-post.widget-area {display:none;}';
最後,使用下面的代碼給DOM添加樣式標簽。這段代碼會馬上執行,側邊欄和底部的書籍欄目會消失。
document.documentElement.appendChild(styleTag);
上面的幾個過程是隱藏頁面元素的必須過程。
回到Xcode,創建一個新文件File->New->File->iOS->Other->Empty並命名為hideSection.js。添加如下代碼。
var styleTag = document.createElement("style"); styleTag.textContent = 'div#sidebar, .after-post.widget-area {display:none;}'; document.documentElement.appendChild(styleTag);
在ViewController中,替換init()中方法為如下:
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的數據將會被打印到命令行。
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。粘貼如下代碼。
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。你應該會看到如下代碼。
@IBOutlet weak var recentPostsButton: UIBarButtonItem!
在viewDidLoad()方法底部,添加如下代碼。我們希望這個按鈕一直不可點,直到posts數組有了數據。
recentPostsButton.enabled = false
在ViewController,import語句下面添加如下代碼。
let MessageHandler = "didGetPosts"
在類文件中添加如下屬性。
var postsWebView: WKWebView?
在viewDidLoad()底部添加如下代碼。
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) postsWebView!.loadRequest(NSURLRequest(URL:NSURL(string:"http://www.appcoda.com")!))
這裡我們像之前一樣導入一個JavaScript文件,我們只希望DOM被構建好時及.AtDocumentEnd時被注入一次。我們也將MessageHandler加入了WKWebViewConfiguration作為WKWebView初始化的配置。
更新類聲明,遵循WKScriptMessageHandler協議。
class ViewController: UIViewController, WKNavigationDelegate, WKScriptMessageHandler
我們建立一個模型(model)文件來保存文章數據。創建一個文件File->New->File->iOS->Source->Cocoa Touch Class。命名為Post並作為NSObject的子類。在類中粘貼如下代碼。
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類中添加如下變量。
var posts: [Post] = []
添加WKScripMessageHandler協議必須遵守的方法。
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添加如下代碼。
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。
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語句下面。
let PostSelected = "postSelected"
當點擊一個cell的時候,我們將發送一個通知。上面的常量就是這個通知的名字。
在PostsTableViewController中添加如下方法。
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()方法底部添加如下代碼。
NSNotificationCenter.defaultCenter().addObserver(self, selector: "postSelected:", name: PostSelected, object: nil)
上述代碼將這個ViewController設置為了cell點擊發送的通知的觀察者(observer)。
在ViewController中添加如下方法。
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。按照下面的代碼編輯這個方法。
@IBAction func cancel(sender: UIBarButtonItem) { dismissViewControllerAnimated(true, completion: nil) }
現在你應該有一個取消按鈕了,它可以用來隱藏這個Table view。
新的WebKit框架使得開發者能夠讓App和網頁內容之間實現無縫交互。我們學習了如何自定義網頁樣式。從網頁中提取數據,並在App中使用這些數據。
如果你的App只是一個網頁版App的容器,使用WebKit框架吧!它將帶來如原生App般的性能和操作體驗。WebKit框架將會為這些體驗不好的App力挽狂瀾。
如果你想了解更多關於此框架的內容,這個WWDC視頻將是個非常好的開始。
你可以在這裡下載完整項目。
(本文為CocoaChina組織翻譯,本譯文權利歸譯者所有,未經允許禁止轉載。)