ios9學習系列:Search API
介紹
在WWDC 2015會議上,蘋果官方公布了iOS9。除開許多新的特性和增強功能,這次升級也給了開發(fā)者們一個機會讓他們的app里的內(nèi)容能通過Spotlight搜索功能被發(fā)現(xiàn)和使用。在iOS9中可用的新APIs允許你去索引APP里面的內(nèi)容或者界面狀態(tài),通過Spotlight來讓用戶使用。 這些新的搜索APIs的三大組件為:
- 
    
NSUserActivity 類, 它是為可被看見的APP內(nèi)容而設(shè)計的
 - 
    
Core Spotlight 框架, 為任何APP內(nèi)容而設(shè)計的
 - 
    
web markup,為這一類型的APP設(shè)計的,就是APP的內(nèi)容在某個網(wǎng)站上有鏡像
 
在這個教程里,我將會向你展示可以怎樣在你的應(yīng)用中使用NSUserActivity類以及 Core Spotlight 框架。
準備工作
這個教程需要你運行在Xcode7 和OSX 10.10系統(tǒng)或更后的系統(tǒng)。為了緊跟我的步伐,還需要你去GitHub上下載初始工程。
1.使用 NSUserActivity
在這個教程的開始部分,我將會給你展示怎樣通過NSUserActivity類來索引一個APP里的內(nèi)容。這個API也是在Handoff里使用的那一個,handoff是在去年iOS8中介紹的功能,它用于保存和復原一個應(yīng)用的當前狀態(tài)。
如果你之前沒有使用過NSUserActivity,那么我建議你在開始這個教程之前先閱讀我的這篇教程 ,它覆蓋了Handoff和NSUserActivity的基礎(chǔ)內(nèi)容。
在開始寫代碼之前,打開初始工程,然后在iOS模擬器上或者測試機上運行APP。在這個階段,你會看到這個APP簡單地展示了一個有著4個TV節(jié)目的列表,以及每個節(jié)目的詳細頁面。


首先,打開工程然后到DetailViewController.swift文件。把DetailViewController類里configureView方法里的內(nèi)容替換為如下:
- func configureView() {
 - // Update the user interface for the detail item.
 - if self.nameLabel != nil && self.detailItem != nil {
 - self.nameLabel.text = detailItem.name
 - self.genreLabel.text = detailItem.genre
 - let dateFormatter = NSDateFormatter()
 - dateFormatter.timeStyle = .ShortStyle
 - self.timeLabel.text = dateFormatter.stringFromDate(detailItem.time)
 - let activity = NSUserActivity(activityType: "com.tutsplus.iOS-9-Search.displayShow")
 - activity.userInfo = ["name": detailItem.name, "genre": detailItem.genre, "time": detailItem.time]
 - activity.title = detailItem.name
 - var keywords = detailItem.name.componentsSeparatedByString(" ")
 - keywords.append(detailItem.genre)
 - activity.keywords = Set(keywords)
 - activity.eligibleForHandoff = false
 - activity.eligibleForSearch = true
 - //activity.eligibleForPublicIndexing = true
 - //activity.expirationDate = NSDate()
 - activity.becomeCurrent()
 - }
 - }
 
在view controller里,配置label的代碼是不變的,讓我們來一步一步分析 user activity 代碼:
- 
    
使用唯一標識符 com.tutsplus.iOS-9-Search.displayShow創(chuàng)建一個新的NSUserActivity對象。 這個工程已經(jīng)被配置成確保使用這個標識符時要保證它不會被改變。
 - 
    
然后為這個user activity 分配一個userInfo字典。它將會在后面被用來修復應(yīng)用的狀態(tài)。
 - 
    
給activity的title屬性賦予了一個字符串值。這就是將會在Spotlight 搜索結(jié)果里出現(xiàn)的內(nèi)容。
 - 
    
為了確??伤褜さ膬?nèi)容不僅止限于應(yīng)用的標題,你也要提供一系列的關(guān)鍵字。在上面的代碼段中,關(guān)鍵字列表中包含了每個節(jié)目的名字以及它的類型。
 - 
    
接下來,你向NSUserActivity對象賦予一些屬性來告訴操作系統(tǒng)你想讓這個user activity用來做什么。在這個教程中,我們只是查看搜索組件的API 因此我們把Handoff禁用掉然后把search開啟。
 - 
    
最后, 調(diào)用user activity的becomeCurrent方法,就在此時它自動的被加入到了設(shè)備的搜索結(jié)果索引中。
 
在以上的實現(xiàn)代碼中,你可能注意到了兩條被注釋的語句。盡管我們不會在這個教程中使用這些屬性,但是了解每個屬性是做什么用的也是很重要的。
- 
    
在上面的實現(xiàn)代碼中,每個節(jié)目的user activity和 搜索結(jié)果都是僅當應(yīng)用曾經(jīng)被打開過時而創(chuàng)建的。當你讓你的user activity有eligibleForPublicIndexing屬性時,Apple就開始從用戶的搜索結(jié)果當中觀察這個特殊activity的作用和交互了。如果這個搜索結(jié)果是被很多用戶所使用的,Apple就提升這個user activity到它自己的云索引(cloud index)中。一旦這個user activity在這個云索引中了,它就可以被所有安裝過你的應(yīng)用的人搜索得到,而不管他們是否有打開過那些內(nèi)容。這個屬性只有當且僅當activities能被你應(yīng)用的所有用戶使用時才能被設(shè)置為true。
 - 
    
一個user activity 可以有一個可選的屬性expirationDate。 當這個屬性被設(shè)置時,你的user activity 只會在設(shè)置的時期之前才會展示在搜索結(jié)果里。
 
現(xiàn)在你已經(jīng)知道了怎樣創(chuàng)建一個可以在Spotlight中展示搜索結(jié)果的NSUserActivity,現(xiàn)在就來實驗吧。編譯運行你的APP,然后在你的應(yīng)用中打開一些節(jié)目。做完這些后,返回到home頁面(在iOS 模擬器中按 Command-Shift-H)然后向下掃或者滑動到最左邊的屏幕就可以拉起搜索框視圖。
在搜索框里填入某個你已經(jīng)打開了的節(jié)目的標題,你將會在搜索結(jié)果里看到它被顯示出來,如下圖。

另外的,輸入某個你已經(jīng)打開了的節(jié)目的類別。歸功于你已經(jīng)對user activity賦予了關(guān)鍵字信息,這也會導致節(jié)目將會在搜索結(jié)果列表里被列舉出來。

你應(yīng)用的內(nèi)容被操作系統(tǒng)正確的索引出來并且結(jié)果就展現(xiàn)在Spotlight 里。但是,當你輕觸一個搜索結(jié)果時,你的應(yīng)用并不會帶領(lǐng)用戶進入他們想要的搜索結(jié)果里面去,而只是簡單地拉起這個應(yīng)用。
#p#
幸運的是,通過 Handoff, 你可以利用NSUserActivity類來復原應(yīng)用里的正確狀態(tài)。為了使這成為可能我們需要實現(xiàn)兩個方法。
如下所示在AppDelegate類里實現(xiàn) application(_:continueUserActivity:restorationHandler:) 方法:
- func application(application: UIApplication, continueUserActivity userActivity: NSUserActivity, restorationHandler: ([AnyObject]?) -> Void) -> Bool {
 - let splitController = self.window?.rootViewController as! UISplitViewController
 - let navigationController = splitController.viewControllers.first as! UINavigationController
 - navigationController.topViewController?.restoreUserActivityState(userActivity)
 - return true
 - }
 
- override func restoreUserActivityState(activity: NSUserActivity) {
 - if let name = activity.userInfo?["name"] as? String,
 - let genre = activity.userInfo?["genre"] as? String,
 - let time = activity.userInfo?["time"] as? NSDate {
 - let show = Show(name: name, genre: genre, time: time)
 - self.showToRestore = show
 - self.performSegueWithIdentifier("showDetail", sender: self)
 - }
 - else {
 - let alert = UIAlertController(title: "Error", message: "Error retrieving information from userInfo:\n\(activity.userInfo)", preferredStyle: .Alert)
 - alert.addAction(UIAlertAction(title: "Dismiss", style: .Cancel, handler: nil))
 - self.presentViewController(alert, animated: true, completion: nil)
 - }
 - }
 
在寫這篇文章的當下,Xcode7(Beta3)的最新版本有一個問題那就是一個用于修復的user activity的 userInfo 屬性會變成空。這就是為什么我會處理errors以及展示一個userInfo(被操作系統(tǒng)返回的)信息的警告。
再次編譯運行你的APP,搜索一個節(jié)目。當你在搜索結(jié)果里輕觸一個節(jié)目時,APP將會直接將你帶到詳細信息的view controller并展示出你選擇的節(jié)目的當前信息。

2.使用Core Spotlight 框架
另外一些在iOS9中能使你的內(nèi)容可被用戶搜索得到的APIs就是Core Spotlight 框架。這個框架有一個類似數(shù)據(jù)庫的設(shè)計并且能夠給你提供更多的關(guān)于你想被搜索到的內(nèi)容的信息。
在你可以使用Core Spotlight框架之前,我們需要把這個工程同這個框架鏈接起來。在Project Navigator中,選中這個工程然后打開最上面的Build Phases欄目。接下來,展開 Link Binary With Libraries 區(qū)域然后點擊加號按鈕。在彈出的菜單中,搜索 CoreSpotlight 然后把你的工程跟這個框架鏈接起來。重復這些步奏來鏈接 MobileCoreServices 框架。

接下來,為了確保我們的APP提供的搜索的結(jié)果確實來自于Core Spotlight,在你的測試機或者模擬器上刪除你的應(yīng)用然后在DetailViewController類中注釋掉下面的這條語句:
- activity.becomeCurrent()
 
最后,打開MasterViewController.swift然后在Show結(jié)構(gòu)體定義之前添加下面的語句:
- import CoreSpotlight
 - import MobileCoreServices
 
接下來,在MasterViewController類的viewDidLoad方法里添加下面的代碼:
- var searchableItems: [CSSearchableItem] = []
 - for show in objects {
 - let attributeSet = CSSearchableItemAttributeSet(itemContentType: kUTTypeItem as String)
 - attributeSet.title = show.name
 - let dateFormatter = NSDateFormatter()
 - dateFormatter.timeStyle = .ShortStyle
 - attributeSet.contentDescription = show.genre + "\n" + dateFormatter.stringFromDate(show.time)
 - var keywords = show.name.componentsSeparatedByString(" ")
 - keywords.append(show.genre)
 - attributeSet.keywords = keywords
 - let item = CSSearchableItem(uniqueIdentifier: show.name, domainIdentifier: "tv-shows", attributeSet: attributeSet)
 - searchableItems.append(item)
 - }
 - CSSearchableIndex.defaultSearchableIndex().indexSearchableItems(searchableItems) { (error) -> Void in
 - if error != nil {
 - print(error?.localizedDescription)
 - }
 - else {
 - // Items were indexed successfully
 - }
 - }
 
在驗證這段代碼之前,我們先過一遍for循環(huán)里的每一步。
- 
    
你創(chuàng)建一個 CSSearchableItemAttributeSet 對象, 給這個項目傳入一個內(nèi)容類型(content type)。例如,如果你的搜索結(jié)果鏈接到一張照片,那么你就應(yīng)該傳入kUTTypeImage常量。
 - 
    
給屬性組合(attribute set)的title屬性賦予一個節(jié)目的名字。就如同 NSUserActivity一樣,這個標題就是將在搜索結(jié)果列表的最頂端出現(xiàn)的那個。
 - 
    
接下來,創(chuàng)建一個描述性字符串然后把它賦值給可搜索的屬性組合(attribute set)的contentDescription屬性。這個字符串將會在Spotlight中搜索結(jié)果的標題下方出現(xiàn)。
 - 
    
就像在NSUserActivity當中創(chuàng)建的那樣,創(chuàng)建一個來自于搜索結(jié)果的關(guān)鍵字數(shù)組。
 - 
    
最后,創(chuàng)建一個有著唯一項目標識符的,唯一域標識符(用來聚集CSSearchableItem項目)的,和一個屬性組合(attribute set)的CSSearchableItem,與NSUserActivity不同的是, NSUserActivity 從搜索結(jié)果中返回user activity, 當你的搜索結(jié)果被用戶選中時,為CSSearchableItem設(shè)置的所有唯一標識符信息就是你可以從操作系統(tǒng)那里得到的唯一信息。 你需要利用這些標識符來復原你的應(yīng)用回到正確狀態(tài)。
 
一旦你為每個TV節(jié)目創(chuàng)建了一個CSSearchableItem項目時,你利用 indexSearchableItems(_:completionHandler:) 方法和默認的CSSearchableIndex對象來索引它們。
編譯運行你的APP,你所有的節(jié)目將會被Spotlight索引到。去到搜索頁面然后搜索其中一個節(jié)目。

Core Spotlight搜索結(jié)果會被跟NSUserActivity里一樣的那個方法所處理,但是過程有一些輕微區(qū)別。當一個CSSearchableItem項目在搜索結(jié)果里被選中時,系統(tǒng)為你創(chuàng)建一個包含選中項目的唯一標識符信息的NSUserActivity對象。
在你的 app delegate的 application(_:continueUserActivity:restorationHandler:)方法中,可以利用下面的實現(xiàn)代碼從Core Spotlight 搜索結(jié)果中獲取你要的信息:
- if userActivity.activityType == CSSearchableItemActionType {
 - if let identifier = userActivity.userInfo?[CSSearchableItemActivityIdentifier] as? String {
 - // Use identifier to display the correct content for this search result
 - return true
 - }
 - }
 
使用Core Spotlight框架來索引APP內(nèi)容的一個良好的實踐就是當項目不再被需要的時候刪除它們。CSSearchableIndex類提供了三種方法來刪除可搜索項目:
- 
    
deleteAllSearchableItemsWithCompletionHandler(:)
 - 
    
deleteSearchableItemsWithDomainIdentifiers(:completionHandler:)
 - 
    
deleteSearchableItemsWithIdentifiers(_:completionHandler:)
 
#p#
作為一個示例,添加下面代碼到MasterViewController類里的viewDidLoad方法:
- CSSearchableIndex.defaultSearchableIndex().deleteSearchableItemsWithDomainIdentifiers(["tv-shows"]) { (error) -> Void in
 - if error != nil {
 - print(error?.localizedDescription)
 - }
 - else {
 - // Items were deleted successfully
 - }
 - }
 
再一次的編譯運行你的應(yīng)用。當你想要搜索任何節(jié)目時,不會有任何結(jié)果返回回來,因為它們已經(jīng)在索引當中被刪除掉了。
3.聯(lián)合NSUserActivity和 Core Spotlight
另一個在iOS9中NSUserActivity類的新增特性就是contentAttributeSet屬性。這個屬性允許你賦予一個CSSearchableItemAttributeSet, 正如你先前創(chuàng)建的那個。這個屬性集合(attribute set)允許NSUserActivity對象的搜索結(jié)果可以展示如同 Core Spotlight搜索結(jié)果那樣的相同數(shù)量的詳細信息。
首先向DetailViewController.swift中最頂部添加下面的imports:
- import CoreSpotlight
 - import MobileCoreServices
 
接下來,用下面的實現(xiàn)代碼更新DetailViewController類的configureView方法:
- func configureView() {
 - // Update the user interface for the detail item.
 - if self.nameLabel != nil && self.detailItem != nil {
 - self.nameLabel.text = detailItem.name
 - self.genreLabel.text = detailItem.genre
 - let dateFormatter = NSDateFormatter()
 - dateFormatter.timeStyle = .ShortStyle
 - self.timeLabel.text = dateFormatter.stringFromDate(detailItem.time)
 - let activity = NSUserActivity(activityType: "com.tutsplus.iOS-9-Search.displayShow")
 - activity.userInfo = ["name": detailItem.name, "genre": detailItem.genre, "time": detailItem.time]
 - activity.title = detailItem.name
 - var keywords = detailItem.name.componentsSeparatedByString(" ")
 - keywords.append(detailItem.genre)
 - activity.keywords = Set(keywords)
 - activity.eligibleForHandoff = false
 - activity.eligibleForSearch = true
 - //activity.eligibleForPublicIndexing = true
 - //activity.expirationDate = NSDate()
 - let attributeSet = CSSearchableItemAttributeSet(itemContentType: kUTTypeItem as String)
 - attributeSet.title = detailItem.name
 - attributeSet.contentDescription = detailItem.genre + "\n" + dateFormatter.stringFromDate(detailItem.time)
 - activity.becomeCurrent()
 - }
 - }
 
最后一次編譯運行APP,然后打開一些節(jié)目。當你搜索一個節(jié)目時,你將會看到你的結(jié)果,伴隨NSUserActivity的創(chuàng)建,擁有和Core Spotlight 搜索結(jié)果相同級別的細節(jié)信息。
 
總結(jié)
在這個教程中,你學習到了使用NSUserActivity類和 Core Spotlight框架來使你的應(yīng)用里的內(nèi)容可被iOS Spotlight 索引。我也向你展示了怎樣使用這兩個APIs在你的應(yīng)用里索引內(nèi)容以及當一個搜索結(jié)果被用戶選中時怎樣復原你的應(yīng)用的狀態(tài)。
在iOS9中介紹的新的搜索APIs使用都很方便而且可以使你的應(yīng)用中的內(nèi)容更簡單的被用戶發(fā)現(xiàn)和接觸。一如既往的,如果你有任何評論或問題,在下方的評論框里留言。















 
 
 






 
 
 
 