博客 / 詳情

返回

如何使用 iOS 9 的 Core Spotlight 框架

作者:AppCoda,原文鏈接,原文日期:2015-12-22
譯者:BigbigChai;校對:walkingway;定稿:CMB

每一代 iOS 都會為全球的開發者們帶來新鮮的“小玩意兒”和對現有技術進行提升。顯然,最新的 iOS 9 也不例外,開發者們擁有了全新的框架和 APIs 以方便調用、這可以顯著地提升應用程序的水平。Core Spotlight 框架就是其中之一,它包含了許多優秀 APIs,開發者可以很方便地應用在工程中。

Core Spotlight(CS)框架屬於一個更大的 API 集合 Search APIs,它讓開發者們可以地將應用變得更容易被發現,以及訪問起來更加便利。這在以前的 iOS 版本里是不可想象的。Search APIs 讓用户和應用之間的聯繫更加緊密。用户可以更迅速地訪問應用,同時應用也能更主動及時地響應用户。除 Core Spotlight 以外,iOS 9 其他新的搜索功能還包括(僅供參考):

  1. NSUserActivity 類的新方法和屬性(負責保存應用的狀態以便稍後恢復)。

  2. web markup 讓網頁的內容在設備上可被搜索。

  3. 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 的細節之前,我們會執行兩個明確的任務:

  1. 加載電影數據並展示在 tableView 裏。

  2. 在詳情視圖控制器裏展示被選中電影的數據。

儘管在初始項目裏實現以上任務能讓你更快地開始本文主題的學習,但我並沒有這麼做,原因很簡單:我堅信,通過對 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 索引數據是最後一步。正常情況下涉及以下步驟(包括索引):

  1. 為每個數據片段設置屬性,例如一部電影(CSSearchableItemAttributeSet 對象)。

  2. 利用上一步獲得的屬性為每個數據片段初始化一個可搜索項目(CSSearchableItem 對象)。

  3. 把所有可搜索項添加到一個數組裏。

  4. 利用以上數組為 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 typeCSSearchableItemActionType 是必要的。坦白地講,這麼做並不重要,但假設應用需要處理多個 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 搜索時能發現這些數據。也説明了應用該如何處理選中的搜索結果,並展現特定的數據給用户。在實現這些特性一定能大幅提升用户體驗,因此你應該認真地考慮在現有的和將來的項目中添加這些特性。我們又到了説再見的時候,希望這篇文章對你有幫助!祝開心!

作為參考,你可以下載完整項目

本文由 SwiftGG 翻譯組翻譯,已經獲得作者翻譯授權,最新文章請訪問 http://swift.gg。

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.