如何使用 iOS 9 的 Core Spotlight 框架
每一代 iOS 都會為全球的開發者們帶來新鮮的“小玩意兒”和對現有技術進行提升。顯然,最新的 iOS 9 也不例外,開發者們擁有了全新的框架和 APIs 以方便調用、這可以顯著地提升應用程序的水平。Core Spotlight 框架就是其中之一,它包含了許多優秀 APIs,開發者可以很方便地應用在工程中。
Core Spotlight(CS)框架屬于一個更大的 API 集合 Search APIs,它讓開發者們可以地將應用變得更容易被發現,以及訪問起來更加便利。這在以前的 iOS 版本里是不可想象的。Search APIs 讓用戶和應用之間的聯系更加緊密。用戶可以更迅速地訪問應用,同時應用也能更主動及時地響應用戶。除 Core Spotlight 以外,iOS 9 其他新的搜索功能還包括(僅供參考):
- NSUserActivity 類的新方法和屬性(負責保存應用的狀態以便稍后恢復)。
- web markup 讓網頁的內容在設備上可被搜索。
- universal links 允許從網頁內容里的鏈接直接打開應用。
我們不會在這篇文章里討論以上三項,但會詳細地介紹 Core Spotlight 框架。但開始之前,我們先來搞清楚這個框架的用途。
Core Spotlight 框架 讓應用里的數據在 Spotlight 可搜索 ,然后把與應用相關的搜索結果與系統返回的其他結果一同展示出來。這令人印象深刻并具有革命性,因為這是用戶首次可以搜索到除 Apple 官方應用外、任意應用中的數據,然后與之進行交互。用戶可以與自定義應用的相關搜索結果進行交互的意思是:不但在搜索結果項被選中時會自動啟動應用,而且開發者們也能引導用戶跳轉到特定視圖控制器,用來展示 Spotlight 中被選擇的數據。
從開發者的角度看來,集成 Core Spotlight 框架和使用它的 API 并不復雜。正如本教程隨后會介紹的那樣,只需要幾行代碼就能搞定。整個過程的重點在于開發者需要“請求” iOS 去索引他們應用里的數據,并且這些數據必須預先以特定的方式來表示。
鑒于這是一篇關于 Core Spotlight 框架的教程,我不打算在簡介部分過于詳細。如果你有興趣學習如何實現一些我個人覺得非常棒的功能,那么請繼續閱讀。我相信,當你讀完之后,就能很輕松地讓你的應用支持 Spotlight 搜索。
關于示例應用
為了深入剖析本節主題的細節部分,我們還是一如既往的借助實例應用來研究。在本教程,我們的應用會展示一系列的數據,這些數據能在設備(或者模擬器)的 Spotlight 中搜索到。盡管這只是一個大的藍圖,但再補充一下應用程序的細節也是很必要的。
我們的示例應用展示了一些 電影 及相關信息,例如簡介、導演、演員、評價,等等。所有的電影數據會展示在 tableView 里,當點擊某一行時,被選中電影的詳情會展示在一個新的視圖控制器里。沒有更復雜的功能了,這種功能和數據就足以讓我們了解 Core Spotlight API 是如何工作的。再補充一點,我們數據的來源是 IMDB ,我是從這里獲取示例數據的。
你可以先看看下面的動圖,大致對這個示例應用有個初步印象
這個教程里我們有兩個目標: 最首要的是在 Spotlight 中能搜索到應用的所有電影數據。這樣,當用戶搜索關鍵詞時,應用中涉及到該電影相關的數據會展示出來。設置這些關鍵詞也是稍后的工作之一,因為定義它們(關鍵詞)也是我們的職責。
點擊搜索的電影結果,會啟動應用,接著我們來完成第二個目標。如果什么都不做,就會加載默認的視圖控制器并呈現給用戶,在我們的例子里就是那個包含了電影列表的 tableview。但是我們想要兼顧用戶體驗的話,這并不是一種好的設計;更好的方案是我們的應用應該在 Spotlight 中展示選中電影的詳細信息,而這正是我們最終要實現的。總而言之,我們不僅要在 Spotlight 中可以搜索電影數據,還要把相關搜索結果所對應的電影詳情展現出來。通過下面示例的學習,你基本就懂了。
為了立即可以開始工作,你可以先 下載初始工程 。在這個工程里,主要包含以下幾部分:
- UI 部分以及所有必要的 IBOutlet 屬性已經設置完成。
- 實現了基本的 tableView
- 所有的電影數據、以及每部電影的封面(一共5張)都存放在 a.plist 文件里。
假如你對 plist 文件里面每部電影包含的信息類型感興趣,下面的截圖能清晰地說明一切:
在深入了解 Core Spotlight API 的細節之前,我們會執行兩個明確的任務:
- 加載電影數據并展示在 tableView 里。
- 在詳情視圖控制器里展示被選中電影的數據。
盡管在初始項目里實現以上任務能讓你更快地開始本文主題的學習,但我并沒有這么做,原因很簡單:我堅信,通過對 demo 應用的極其數據內容的探索會讓你更直觀地明白特定數據是如何在 Spotlight 里被搜索的。不過不用擔心,準備工作都不多且都能快速完成。
加載和展示示例數據
好的,讓我們開始吧!假設現在你已經下載了初始工程并查看了包含電影數據的 plist 文件。在 MoviesData.plist 文件里,你會看到總共五項在 IMDB 網站里隨機選取的示例電影數據。我們的第一個目標是把 .plist 文件里的數據加載到一個數組里,然后展示在 tableView 里。
直接進入代碼部分,打開最主要的 ViewController.swift 文件,并在類的頂部聲明一個屬性:
var moviesInfo: NSMutableArray!
所有的電影都會加載到這個數組里,每一部電影都會以鍵值對字典的形式與 .plist 文件相對應。
我們先來寫一個簡單的自定義方法來加載數據。正如下面所展示的,首先確保了 .plist 文件的存在,然后就可以用該文件的內容來初始化數組。
func loadMoviesInfo() {
if let path = NSBundle.mainBundle().pathForResource("MoviesData", ofType: "plist") {
moviesInfo = NSMutableArray(contentsOfFile: path)
}
}
接著,我們要在 viewDidLoad() 里調用這個方法。要確保在 configureTableView() 方法之前調用它,即要按照以下代碼片段的展示:
override func viewDidLoad() {
super.viewDidLoad()
// 從文件里加載電影數據。
loadMoviesInfo()
configureTableView()
navigationItem.title = "Movies"
}
我們本可以直接在 viewDidLoad() 方法里面加載文件內容,而無需創建自定義方法。但是我喜歡整齊的代碼,即使對于這么簡單的一個小功能,創建一個自定義方法還是好很多。
我們既然知道了應用會在每次啟動時加載電影數據,就可以繼續修改當前 tableView 的實現讓它展示我們的電影。這里并沒有太多需要做的:我們會根據電影數量定義 tableView 行數,然后把適合的數據展示在 Cell 里。
先從行數開始,很明顯行數需要與電影數目相等。然而,我們首先要確保有電影可以展示,不然當數組沒有加載到文件內容時應用會崩潰。
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if moviesInfo != nil {
return moviesInfo.count
}
return 0
}
最后,讓我們顯示電影數據。出于演示的目的,在起始項目里你能找到一個 UITableViewCell 的子類 MovieSummaryCell ,還有與其對應的 .xib 文件代表了單個電影 Cell:
這樣的 Cell 展示了每部電影的圖片、標題、簡介、以及評分。所有的 UI 控制器都有相對應的 IBOutlet 屬性,你可以在 MovieSummaryCell.swift 文件里找到它們:
@IBOutlet weak var imgMovieImage: UIImageView!
@IBOutlet weak var lblTitle: UILabel!
@IBOutlet weak var lblDescription: UILabel!
@IBOutlet weak var lblRating: UILabel!
以上的命名方式表明了每個屬性的功能,搞清楚后,我們利用它們來展示電影的詳情。回到 ViewController.swift 文件,按照下面的代碼片段更新 tableView 方法:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("idCellMovieSummary", forIndexPath: indexPath) as! MovieSummaryCell
let currentMovieInfo = moviesInfo[indexPath.row] as! [String: String]
cell.lblTitle.text = currentMovieInfo["Title"]!
cell.lblDescription.text = currentMovieInfo["Description"]!
cell.lblRating.text = currentMovieInfo["Rating"]!
cell.imgMovieImage.image = UIImage(named: currentMovieInfo["Image"]!)
return cell
}
currentMovieInfo 字典并不是必須的,但它可以讓代碼變得更加簡單。
現在能如你所愿地第一次嘗試運行這個應用了。可以看到一些電影詳情在 tableView 中羅列出來。到現在為止都是大家很熟悉的步驟,下面讓我們直接開始第二個準備步驟:展示所選電影的詳情信息。
展示數據詳情信息
我們在 ViewController 類的 tableView 里選中電影的詳情,將通過 MovieDetailsViewController 類來展示,對應的場景在 Interface Builder 里已經寫好,所以現在有兩個任務: 從 ViewController 里傳遞對應的電影字典到這個類里,然后把字典里的值傳遞到適當的 UI 控制器里,而這些 IBOutlet 屬性都已經被聲明并且正確地連線了。
說到字典,讓我們在 MovieDetailsViewController 類的頂部做出以下聲明:
var movieInfo: [String: String]!
先暫時回到 ViewController.swift ,看看當一行電影數據被點擊的時候,我們需要做些什么。這時需要了解被選中行的索引,以便從 movieInfo 數組中選擇恰當的字典,并在 Segue(名為 idSegueShowMovieDetails )執行的時候傳遞給下一個視圖控制器。從 tableView 的代理方法里獲取索引很簡單,但我們仍需要一個自定義屬性來保存它。因此在 ViewController 類的頂部我們需要聲明:
var selectedMovieIndex: Int!
然后,我們需要按照以下方法處理 tableView 的行選擇:
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
selectedMovieIndex = indexPath.row
performSegueWithIdentifier("idSegueShowMovieDetails", sender: self)
在這兒,我們做兩件非常簡單的事情:首先,把選中的行索引保存在自定義的屬性里,然后執行展示電影詳情的 Segue。然而這還不夠,因為還沒有從 moviesInfo 數組里選擇合適的電影字典,而且也還沒把任何數據傳遞給 MovieDetailsViewController 類。那么我們需要做些什么呢? 那就是重寫 prepareForSegue:sender: 方法并完成上述功能。
oerride func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if let identifier = segue.identifier {
if identifier == "idSegueShowMovieDetails" {
let movieDetailsViewController = segue.destinationViewController as! MovieDetailsViewController
movieDetailsViewController.movieInfo = moviesInfo[selectedMovieIndex] as! [String : String]
}
}
}
足夠簡單了吧。我們只是通過這個 Segue 的 destinationViewController 屬性獲得 MovieDetailsViewController 實例,然后把對應的電影字典賦值給在本部分最開始聲明的 movieInfo 屬性。
現在,重新打開 MovieDetailsViewController.swift 文件,其中只有一個自定義方法。通過該該方法,將 movieInfo 字典中的值分配給相應的 UI 控制器,至此我們的工作就結束了。以下是一個簡單的實現,我就不詳述了:
func populateMovieInfo() {
lblTitle.text = movieInfo["Title"]!
lblCategory.text = movieInfo["Category"]!
lblDescription.text = movieInfo["Description"]!
lblDirector.text = movieInfo["Director"]!
lblStars.text = movieInfo["Stars"]!
lblRating.text = movieInfo["Rating"]!
imgMovieImage.image = UIImage(named: movieInfo["Image"]!)
}
最后,讓我們在 viewWillAppear: 方法里調用以上方法:
override func viewWillAppear(animated: Bool) {
...
if movieInfo != nil {
populateMovieInfo()
}
}
這部分就結束了。你可以再運行一下,然后在 tableView 里選擇某電影的時候查看一下電影的詳情。
為 Spotlight 索引數據
使用 iOS 9 的 Core Spotlight 框架讓任何應用的數據都能通過 Spotlight 搜索到。要做到這一點,關鍵在于請求 Core Spotlight API 索引 我們的數據,以便用戶可以搜索到。但無論是我們的應用還是 CS API 都無法判斷數據的類型。因為準備數據并把數據以特定格式提供給 API 是我們的職責。
再解釋一下,我們希望能在 Spotlight 中搜索到的所有數據都必須表現為 CSSearchableItem 對象,然后組織成數組形式提供給 CS API 索引。單個 CSSearchableItem 對象包括一組屬性,它可以讓 iOS 完全掌握被搜索項的細節,類似于哪部分數據應該在搜索時展示(例如,電影的名字、它的圖片和描述信息),還有哪些關鍵詞會觸發包含相關數據的應用在 Spotlight 里出現。單個可被搜索的項目的所有屬性都會展示在一個 CSSearchableItemAttributeSet 對象里,它提供了許多屬性讓我們用于賦值。作為參考,我提供了 官方文檔鏈接 便于你查看所有可用的屬性。
Spotlight 索引數據是最后一步。正常情況下涉及以下步驟(包括索引):
- 為每個數據片段設置屬性,例如一部電影(CSSearchableItemAttributeSet 對象)。
- 利用上一步獲得的屬性為每個數據片段初始化一個可搜索項目(CSSearchableItem 對象)。
- 把所有可搜索項添加到一個數組里。
- 利用以上數組為 Spotlight 索引數據。
我們來按步驟執行,為了實現目標,需要在 ViewController.swift 文件里創建一個名為 setupSearchableContent() 的自定義方法。在實現本部分內容后,你會發現想要搜索全部的數據并不是一件難事。但是,我們不打算一步登天,也不準備一次性把所有的實現都告訴你們;而是把代碼分段,以便你們理解。別擔心,這并不復雜。
在我們實現新方法之前,需要先導入兩個框架:
import CoreSpotlight
import MobileCoreServices
讓我們開始定義新方法,首先聲明一個數組,待會用來收集可被搜索的項目:
func setupSearchableContent() {
var searchableItems = [CSSearchableItem]()
}
現在我們能在循環里讀取每一部電影:
func setupSearchableContent() {
var searchableItems = [CSSearchableItem]()
for i in 0...(moviesInfo.count - 1) {
let movie = moviesInfo[i] as! [String: String]
}
}
我們會為每部電影創建一個 CSSearchableItemAttributeSet 對象,然后設置相應的屬性,這樣在 Spotlight 搜索時就會展示相關的結果。在示例中,我們會指定電影標題、簡介和圖片這部分數據展示給用戶。
func setupSearchableContent() {
var searchableItems = [CSSearchableItem]()
for i in 0...(moviesInfo.count - 1) {
let movie = moviesInfo[i] as! [String: String]
let searchableItemAttributeSet = CSSearchableItemAttributeSet(itemContentType: kUTTypeText as String)
// 設置標題.
searchableItemAttributeSet.title = movie["Title"]!
// 設置電影封面.
let imagePathParts = movie["Image"]!.componentsSeparatedByString(".")
searchableItemAttributeSet.thumbnailURL = NSBundle.mainBundle().URLForResource(imagePathParts[0], withExtension: imagePathParts[1])
// 設置簡介.
searchableItemAttributeSet.contentDescription = movie["Description"]!
}
}
留意在以上代碼片段里我們是如何為電影圖片這個屬性進行賦值的。有兩種方式:一是指定圖片的 URL,二是把圖片作為 NSData 對象。對我們來說,最簡單的方式就是為提供所有電影圖片文件的 URL,眾所周知這些圖片就存在于應用 bundle 里。然而,這種方式需要把每個圖片文件名分成實際名字和擴展名,因此我們利用 String 類的 componentsSeparatedByString: 方法來實現分割操作,剩下的就很簡單了。
現在是時候設置一波關鍵詞了,這樣就能通過 Spotlight 搜索到 App 中的相關數據了。在指定關鍵詞之前要考慮清楚,因為你的決定會影響 App 在 Spotlight 里的搜索結果、這對用戶也很重要。在本例中,我們會把電影所屬的類別及其評星數設置為關鍵詞。
func setupSearchableContent() {
var searchableItems = [CSSearchableItem]()
for i in 0...(moviesInfo.count - 1) {
...
var keywords = [String]()
let movieCategories = movie["Category"]!.componentsSeparatedByString(", ")
for movieCategory in movieCategories {
keywords.append(movieCategory)
}
let stars = movie["Stars"]!.componentsSeparatedByString(", ")
for star in stars {
keywords.append(star)
}
searchableItemAttributeSet.keywords = keywords
}
}
要知道電影的分類在 MoviesData.plist 文件中用一段字符串表示,每部電影之間用逗號分隔。因此很有必要把這段字符串所代表的電影類比分隔出來,然后存在 movieCategories 數組里方便訪問。接著使用內循環把每個值添加到 keywords 數組。對于評星數,我們也執行完全相同的步驟,再次把一個包含所有電影評星數的字符串分隔為許多獨立的部分,最后添加到關鍵詞數組。
需要注意的是最后一行;我們為每部電影的屬性集合設置了關鍵詞。如果漏了這一行,那么 Spotlight 就不會展示任何關于這個應用的搜索結果。
現在我們已經為 Spotlight 設置了屬性和關鍵詞,是時候初始化一個可搜索項目并添加到 searchableItems 數組了:
func setupSearchableContent() {
var searchableItems = [CSSearchableItem]()
for i in 0...(moviesInfo.count - 1) {
...
let searchableItem = CSSearchableItem(uniqueIdentifier: "com.appcoda.SpotIt.\(i)", domainIdentifier: "movies", attributeSet: searchableItemAttributeSet)
searchableItems.append(searchableItem)
}
}
以上的初始化方法接收三個參數:
- uniqueIdentifier : 這個參數唯一地標識了當前在 Spotlight 的可搜索項目。你可以用你喜歡的方式編寫這個標識符,但是要注意一個細節:在這個示例里我們添加了當前電影的索引作為標識符,因為稍后會展示與索引值相匹配的電影詳情。總體來說,在標識符中包含一個指向某些數據的索引是個好主意,這些數據將會用于詳情展示。你稍后會更好地了解電影索引的作用。
- domainIdentifier : 使用這個參數把可搜索項目組成集合。
- attributeSet 這是我們剛剛用于賦值的屬性集合對象。
最后,新的可搜索項目被加到 searchableItems 數組里。
我們最后需要執行的步驟是使用 Core Spotlight API 索引這些項目。通過 for 循環來實現:
func setupSearchableContent() {
...
CSSearchableIndex.defaultSearchableIndex().indexSearchableItems(searchableItems) { (error) -> Void in
if error != nil {
print(error?.localizedDescription)
}
}
}
上面的方法已經功能齊全了,就等調用了。我們會在 viewDidLoad() 方法里調用它:
override func viewDidLoad() {
...
setupSearchableContent()
}
我們現在已經準備好首次使用 Spotlight 搜索電影了。運行應用,退出,然后在 Spotlight 使用之前定義好的任意關鍵詞。你會看見搜索結果展現在眼前。點擊任意搜索結果,會自動啟動相關應用。
實現定點著陸
雖然通過 Spotlight 可以搜索到我們應用中的電影數據這一點令人印象深刻,但還是能更上一層樓。目前,點擊搜索結果,會跳轉到應用首頁 ViewController 界面。但是我們的目標是讓它直接跳轉到電影詳情的視圖控制器,并展示所選擇電影的相關信息。
雖然聽起來比較復雜,但其實相當容易。針對我們這個示例應用則更簡單了。因為我們已經完成了絕大部分的基礎工作,可以很容易地實現展示選中電影的詳情頁面。
這里的主要工作是重寫一個 UIKit 方法 restoreUserActivityState: ,來處理在 Spotlight 被選中的搜索結果。當我們最終想要實現的是,從可搜索的項目標識符中取出該電影在 moviesInfo 數組里的索引值(如果你記得的話,我們在之前的部分動態地創建了這個標識符)。
該方法接受一個 NSUserActivity 對象作為參數。這個對象有一個名為 userInfo 的字典屬性,其中包括了在 Spotlight 中被選中的可搜索項目的標識符。我們通過標識符在 moviesInfo 數組里獲取該電影的索引值,然后展示詳情視圖控制器。就這些。
來看看具體實現:
override func restoreUserActivityState(activity: NSUserActivity) {
if activity.activityType == CSSearchableItemActionType {
if let userInfo = activity.userInfo {
let selectedMovie = userInfo[CSSearchableItemActivityIdentifier] as! String
selectedMovieIndex = Int(selectedMovie.componentsSeparatedByString(".").last!)
performSegueWithIdentifier("idSegueShowMovieDetails", sender: self)
}
}
}
如你所見,首先檢查 activity type 和 CSSearchableItemActionType 是必要的。坦白地講,這么做并不重要,但假設應用需要處理多個 NSUserActivity 對象,那么你就別忘了做這件事(例如,在 iOS 8 首次出現的 Handoff 特性利用了 NSUserActivity 類)。這個標識符是一個儲存在 userInfo 字典里的字符串值。得到這個字符串之后,我們會把它根據點符號(dot symbol)分成不同部分,然后獲取最后一部分,這是被選中的電影在電影集合里的索引。剩下的就很簡單了:給 selectedMovieIndex 屬性賦值然后執行 Segue。剩下的任務就交給我們之前的實現了。
現在打開 AppDelegate.swift 文件。我們需要添加一個新的代理方法。每一次與應用相關的搜索結果在 Spotlight 里被選中的時候,這個方法都會被調用,我們只需調用上面實現的方法,傳遞 user activity 對象即可。來看看具體實現:
func application(application: UIApplication, continueUserActivity userActivity: NSUserActivity, restorationHandler: ([AnyObject]?) -> Void) -> Bool {
let viewController = (window?.rootViewController as! UINavigationController).viewControllers[0] as! ViewController
viewController.restoreUserActivityState(userActivity)
return true
}
在以上代碼片段里,在恢復用戶活動狀態前,我們首先通過 window 屬性獲取到 ViewController 視圖控制器。你還可以利用 NSNotificationCenter 和發送自定義通知來實現,這樣你需要在 ViewController 類里處理通知。顯然第一種方案更為直觀。
這就是全部內容了!我們的示例應用已經完成,那么再運行一次看看在 Spotlight 里搜索電影時會發生什么吧。
總結
iOS 9 最新的搜索 API 對于開發者而言前景廣闊,因為這些 API 能大幅提高應用的曝光度、也更容易被用戶訪問。在本教程里,我們涉及了索引應用數據的所有步驟,最終在 Spotlight 搜索時能發現這些數據。也說明了應用該如何處理選中的搜索結果,并展現特定的數據給用戶。在實現這些特性一定能大幅提升用戶體驗,因此你應該認真地考慮在現有的和將來的項目中添加這些特性。我們又到了說再見的時候,希望這篇文章對你有幫助!祝開心!
來自:http://swift.gg/2016/08/30/core-spotlight-framework/