iOS 系統搜索集成

jopen 8年前發布 | 16K 次閱讀 iOS開發 移動開發

About the Speaker: Jack Nutting

Jack 編寫 Cocoa 應用(以及其前身)已經有 20 多年了。雖然 Objective-C 可以說算是他的“母語”了,但是他也準備好了使用 2014 年 Apple 推出的全新語言—— Swift。Jack 是許多 iOS 和 OSX 編程書籍的共同著者之一,包括最暢銷的那本《Beginning iPhone Development with Swift》。

</div>

@jacknutting

</div>

介紹(0:00)

大家好,我是 Jack。今天我演講的主題是 iOS 中的系統搜索功能,我尤其會著重講解如何在應用中加入 iOS 9 中新引入的全局搜索功能 (global search functionality)。大家都知道,iOS 的系統搜索功能已經存在很久了。你可以借助這項功能檢索已安裝的應用、郵件、聯系人、日歷等多類信息,就我個人而言,我更喜歡用這項功能來啟動應用,而不是從主屏幕中啟動應用。

在 iOS 9 中,這項功能更加有用了,因為你的應用可以提供相關的內容,并讓它們能夠被 iOS 系統搜索到。這項功能不僅對設備上的本地數據有效,而且還對你應用中所顯示的網絡數據有效。你可以展示你從網絡中獲取的數據,當數據讀取完畢后用戶就可以搜索到它,隨后用戶單擊這項內容后,就會打開應用,在本地顯示這些數據。這使得 iOS 系統搜索功能不僅僅只用于搜索已安裝的應用,還可以實現更多的功能。

在本次講座的安排中,我們將會討論幾種在 iOS 9 中為應用添加系統搜索功能的方式,它們所涉及到的技術非常相似。其中之一就是 NSUserActivity ,它的用處基本上是對私有內容和用戶可見的公開內容建立索引。此外還有一個在 iOS 9 中新引入的 Core Spotlight,它能夠使用設備上的私有內容來實現本地搜索。使用這兩種方式的好處就是,它們實際上都允許你能夠以一種更好的方式在搜索結果中顯示諸如圖片之類的數據。

此外我們還將探討 Web 標記,它允許你在所展示的內容中,查找擁有本地組件 (native component) 和 Web 組件的相同內容。你可能會希望你的本地應用能夠有這樣的功能:無論你所擁有的 Web 賬戶存放在何處,你都能夠更新你的 Web 應用數據,并且還能決定所能夠搜索到的數據。最后,我會快速演示一個我創建的簡單代碼庫,在其中我實現了某些例子。我所有的 演示示例代碼 都放在了 Github 上。每一步我都建立了不同的分支,以便為大家提供方便。

NSUserActivity 概覽(3:15)

首先, NSUserActivity 實際上是為支持 Handoff 而引進的一項技術,它允許你創建能夠在多臺 iOS、Mac 設備之間協同工作的應用。其基本思路是在一個特定的時間點保存當前應用的工作狀態,這樣其他相關聯的應用就可以使用這個保存的工作狀態,從而實現再另一臺設備中繼續使用。

在 iOS 9 中,Apple 實際上使用了同樣的技術來實現系統搜索功能。當用戶在你的應用中瀏覽時,你可以通過 ID 將用戶瀏覽的地方標記上不同的狀態,這將允許你在之后重新恢復到這些狀態。為了讓這個功能更容易使用,Apple 對此進行了一點小小的調整,無論你保存了什么樣的用戶活動 (user activity) ,它都將自動建立索引,從而可以被搜索到。

你所能做的就是,決定是否將這些活動標記為公開索引。一個被標記為公開可見的頁面,那么就說明任何使用這個應用的人都可以看到這個頁面。一旦這些公開可搜索項 (searchable item) 中的用戶點擊率達到一定程度,那么 Apple 就會在他們的公開搜索索引中顯示這些搜索項,從而讓它們能夠顯示在其他用戶的 iOS 9 搜索結果中,即使這些用戶并沒有安裝過你的應用。這在很大程度上增強了你應用程序的曝光度,因為人們很有可能會去搜索一些他們所不知道的熱門應用。

func application(application: UIApplication, 
  continueUserActivity userActivity: NSUserActivity,
  restorationHandler: ([AnyObject]?) -> Void) -> Bool

除了實際創建這些 NSUserActivity 對象之外,你還需要實現一些功能來讓系統知道如何對其進行處理。上面這段代碼是一個應用委托方法,它會在合適的時候被調用。在 iOS 9 中,它會當用戶搜索到你的公開項并點擊的時候調用。你可以將用戶直接帶到相應的頁面上。這個方法實際上也是用以實現 Handoff 的方法。因此,如果你已經實現了 Handoff 功能,那么實際上你已經完成了絕大部分的工作了。

userActivity.eligibleForPublicIndexing

我提到過,用戶活動可以設定為公開的,也可以設定為私有的。如果你將其標記為公有的話,那么你就可以增加應用的曝光率,讓那些沒聽說你應用的人有更多機會了解到你的應用,并且實現起來非常簡單,它只是一個在 userActivity 中的一個布爾屬性而已。你只需要給這個屬性設為 true ,就萬事大吉了。

Core Spotlight 概覽(6:48)

另外一個我想討論的重要技術就是 Core Spotlight 了。這同樣也是一個用于系統搜索的技術,并且它在 OS X 上已經存在了很長時間了。Core Spotlight 對于所需要顯示的可搜索項,提供了多種預定義好的類型,比如說圖像、音頻等等。這允許你將應用內容變為可搜索到的,并且可以以更豐富的方式來展現這些內容,因為你不僅可以往搜索內容中添加圖像,還可以給出指定文本的上下文內容等等。Core Spotlight API 與索引數據庫建立了映射,這樣你就可以在合適的時候執行更新、刪除之類的操作。它使用的是私有的用戶數據,因此這些數據只能夠在本機上顯示。在 Core Spotlight 中建立索引的數據是無法被別的機器發現的。

NSUserActivity 和 Core Spotlight 的功能極其相似,但是某些地方也是有所不同的。好處就是在同一個應用中同時實現兩種技術是完全可行的,并且實現難度不是很高,我會在下面進行展示。它們使用的都是同一個應用委托方法,當系統想要展示搜索結果的時候,這個方法就會被調用。不過 Core Spotlight 搜索方式和其他搜索方式還是有不同之處的,因此需要稍微處理一下這些不同的地方。用戶活動中可能會包含有稍微不一樣的東西,這取決于它是被標記為 NSUserActivity 項還是被標記為 Core Spotlight 項,我們接下來也會看到它們是如何進行工作的。

為 Web 內容建立索引(8:52)

為 Web 內容建立索引的主要內容是關于如何正確地在你的網頁上顯示元數據 (metadata) 的。我們再強調一遍,如果你的應用程序非常依賴于 Web 上的數據,那么建立索引應當是你首先需要做的事情。比如說你有一個和 Airbnb 類似的應用。用戶可以在應用內置的 Web 頁上進行搜索,也可以在手機提供的搜索中進行搜索,這兩種搜索方式都應該要顯示出相同的結果,因此你想要將它們集成到一起,減少代碼復用。你所要做的就是進行一些設置,讓網絡搜索返回一些和你應用相關聯的元數據。

為了實現這個功能,你所需要做的其實并不多。首先,你需要使用智能廣告條 (Smart Banners) 或者通用鏈接 (Universal Links) 來創建深度鏈接 (deep linking) 的元數據。Apple 實際上內置了許多不同種類的深度鏈接,比如說 推ter 或者 非死book 應用鏈接等等。因此如果你當前的 Web 應用中通過放置了正確的鏈接實現了這種功能,那么 Apple 就可以找到這些元數據,從而可以在iOS 9 搜索功能中,通過 Web 搜索到這些內容,然后啟動你的應用。Apple 或許會在這個功能更流行之后為其提供更多的支持技術。

另一個內容是結構化數據 (structured data)。這是一個可選的功能,但實際上這也非常炫酷,因為 Apple 支持很多不同的結構化數據模式,從而讓你可以為搜索結果提供額外的數據。其中某些模式可以直接在 iOS 9 搜索結果中直接執行某些操作,甚至無需啟動你的應用就可以完成。例如,假設你正從一個類似于 Airbnb 的應用中尋找一個正在出租房屋的房東的電話號碼。你可以用一個已確定的 meta 標簽對這個聯系人進行標記,隨后他的電話號碼就會以按鈕的形式出現在 iOS 搜索結果中,點擊這個號碼就可以直接撥打他的電話了。

當你要提交應用的時候,你同樣可以幫助 Apple 對你的搜索結果建立索引,比如說你的營銷網站、官方網站等等。如果這些網站都鏈接到了你的主頁,并且這個主頁導向的是有用的深度鏈接的話,那么 Apple 就可以對其建立索引,將這個網站和你的應用關聯起來。我們重復一遍,這個功能不是強制的,但是卻十分有用。

除了 Web 元數據之外,剩下你所需要實現的功能就是在你的 iOS 應用中,為其添加能夠通過一個給定的 URL 啟動應用的能力。這與 Core Spotlight 和 NSUserActivity 所使用的委托方法不同,這里用的是 func application(app:openURL:options:) 。在這個方法中你可以獲取一個 URL,然后嘗試打開這個鏈接,然后根據用戶的請求來顯示正確的應用內容。點擊搜索結果將會跳轉到你的原生應用中應當成為一個常識,因為 Apple 會向你的應用發送這些信息。因此,如果你的應用需要登錄或者注冊才能夠允許用戶訪問里面的內容的話,你需要考慮當用戶從搜索結果中跳轉到應用當中的時候,是否需要延遲強制登錄的提示。

Apple 對于搜索、Web 內容元數據、所支持的模式等還提供了更詳細的說明,你也可以在 開發者中心 找到網站驗證工具,驗證你的網站是否符合要求。

回顧 - 如何實現上述的功能(14:05)

完整的示例代碼都可以 在這里 找到,每個分支對應一個內容。

示例應用配置(14:46)

為了演示如何實現搜索功能,我創建了一個非常簡單的應用,它提供了一個關于老式計算機數據的數據庫,并且可以在全局 iOS 搜索中查詢到這些數據。這個數據庫非常簡單,它只包含有兩個數據。也就是 Apple 2 和 Atari 400。同樣地,代碼也是簡單得超乎你們的想象。詳情視圖控制器將會展示 Computer 結構體類型的詳細信息,同時還包括了其中每個計算機中的一些數據。我同樣還寫了一個名為 ComputerDataSource 的類,它將 plist 文件轉為 Computer 類型。不要在實際中使用這種糟糕的強制解包方法,因為我們只是為了演示,因此簡單一點就好。總之,目前為止這個應用程序是非常簡單的。

創建可搜索的 NSUserActivity 對象(18:06)

接下來的 Step 2 分支中,我添加了 NSUserActivity 的基本實現。這其實非常簡單。當用戶準備瀏覽某個電腦的詳情視圖時,我在 prepareForSegue() 中調用相關方法創建并保存這些 NSUserActivity 對象。

func updateIndexForComputer(computer: Computer) -> NSUserActivity {
  let activity = NSUserActivity(activityType: "com.thoughtbot.retro.computer.show")
  activity.userInfo = ["name": computer.shortDescription]
  activity.title = computer.shortDescription
  activity.keywords = [computer.model, computer.company, computer.cpu]
  activity.eligibleForHandoff = false
  activity.eligibleForSearch = true

activity.becomeCurrent() return activity }</pre></div>

我創建了一個 NSUserActivity 對象,并為其分配了一個 activityType 。這個活動類型應該是你可以識別的類型,命名風格是一個典型的 Apple 命名風格,也就是反向 DNS 風格。隨后我會聲明,這個活動類型將顯示我模型中的某一個項目,然后它會根據我給定的活動類型進行命名。我同樣還添加了一個用戶信息,這是一個字典,其中包含了足夠的信息以便能夠唯一確定它實際要展示的內容。在這個例子中,由于現在我的電腦對象并沒有任何類型的標識符,因此我就只好使用 “shortDescription” 來作為標識符,這個是一個關于計算機制造商以及計算機架構的數據,我希望這不會出問題。接下來還有一些事情,我設置了標題和關鍵詞,這些都是能夠被搜索到的數據。因此,當用戶前往 iOS 9 的搜索界面搜索這些標題或者關鍵詞的時候,所有信息就會展示出來。

我同樣還將 eligibleForHandoff 屬性設為了 false ,因為目前我們并不對使用 Handoff 感興趣。再次強調 ,NSUserActivity 擁有雙重職能。它不僅用于搜索,還可用于 Handoff。由于這里我們并不會實現 Handoff 功能,只需要它的搜索功能,因此最后我們就將 Handoff 支持取消即可。在這種情況下 becomeCurrent() 將會更新 iOS 9 的搜索索引,以便讓這些項目變為可搜索的。最后我們所要做的就是將這個活動返回即可。

latestActivity = updateIndexForComputer(object)

在主視圖控制器中,我用一個稱之為 latestActivity 的屬性來保存這個返回值。這個屬性我實際上并沒有對其進行讀取,但是我發現如果你不將這個返回值保存下來的話,有時就會出現 NSUserActivity 沒有建立索引的情況,因為這時候這個活動被釋放掉了,因此就沒有任何機會在搜索中現身了,這確實有點煩人。

完成之后,我們就可以讓這個內容能夠在 iOS 中搜索到了,但是我們目前還沒有處理當用戶點擊查詢結果的時候所出現的情況。

從搜索結果中恢復應用狀態(22:22)

在 Step 3 分支中,我添加了下面這個委托方法:

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.viewControllers[0].restoreUserActivityState(userActivity)
  // navigationController.topViewController?.restoreUserActivityState(userActivity)
  return true
}

當有人單擊搜索結果時這個方法就會被調用,以處理所點擊的用戶活動。我通過 Split 控制器和導航控制器做了一些瘋狂的事情,這是因為在默認的有多個導航控制器的項目中 Apple 所做的那些奇怪的配置。關鍵是我實際上只想抵達顯示對象列表的根視圖控制器而已。要獲取我們的主視圖控制器,我們需要獲取到第一個導航欄控制器,然后從這個導航欄控制器中獲取第一個視圖控制器。隨后我讓其調用 restoreUserActivityState() 方法。這個方法是定義在 UIViewController 中的,但是亦可以對其進行重寫從而實現一些有趣的功能。在這里我只是委托這個主視圖控制器負責做它所需要做的事情就可以了。

override func restoreUserActivityState(activity: NSUserActivity) {
  guard let name = activity.userInfo?["name"] as? String else {
    NSLog("I can't restore from this activity: (activity)")
    return
  }

let matches = ComputerDataSource().computers.filter { $0.shortDescription == name }

if matches.count == 0 { NSLog("No computer matches name (name)") return } self.computerToRestore = matches[0] self.performSegueWithIdentifier("showDetail", sender: self) }</pre></div>

這里我實現了這個 restoreUserActivityState() 方法。我首先會檢查 name 是否存在,隨后從數據源中抓取計算機清單,使用 filter 方法獲取 shortDescription 相同的計算機,以確保我能夠找到該活動對應的數據,如果找到的話將其存儲到名為 computerToRestore 的屬性中,然而調用 performSegueWithIdentifier() 方法。

主控制器擁有一個前往詳情控制器的 segue,通過這個功能能夠輕松定義詳情控制器的啟動行為,因此我采用這個功能。 prepareForSegue() 的最初版本中將會獲取表視圖當前所選行的 indexPath,隨后基于此進行其他設置就可以。下面是更新后的版本:

override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
  if segue.identifier == "showDetail" {
    if let computer = computerToRestore {
      let controller = (segue.destinationViewController as! UINavigationController).topViewController as! DetailViewController
      controller.detailItem = computer
      controller.navigationItem.leftBarButtonItem = self.splitViewController?.displayModeButtonItem()
      controller.navigationItem.leftItemsSupplementBackButton = true
    computerToRestore = nil
    }
    else if let indexPath = self.tableView.indexPathForSelectedRow {
      ...
    }
  }
}

我設置了一個前提條件,我們會首先查看, computerToRestore 這個屬性是否有值。如果有值,我們就和之前所做的那樣,將 computerToRestore 傳遞給下一層控制器。到目前為止,我們就可以點擊搜索結果后成功地進入到對應的頁面當中了,但是如果我們想給搜索結果增加點東西怎么辦呢?

使用 Core Spotlight 來豐富搜索結果(27:47)

為了豐富搜索結果的顯示,而不僅僅只是顯示應用圖標,我們將使用 Core Spotlight。

func updateIndexForComputer(computer: Computer) -> NSUserActivity {
  // ... setting up activity

let attributeSet = CSSearchableItemAttributeSet(itemContentType: kUTTypeImage as String) attributeSet.title = computer.shortDescription attributeSet.contentDescription = "(computer.shortDescription)\n(computer.cpuDescription)\n(computer.productionStartYear)" attributeSet.thumbnailData = UIImagePNGRepresentation(computer.image)

let item = CSSearchableItem(uniqueIdentifier: computer.shortDescription, domainIdentifier: "retro-computer", attributeSet: attributeSet)

CSSearchableIndex.defaultSearchableIndex().indexSearchableItems([item]) { error in if let error = error { NSLog("indexing error: (error)") } }

// activity.becomeCurrent() return activity }</pre></div>

首先我想指出的是,我注釋掉了最底下的 activity.becomeCurrent() 語句。我想要確保我們用 Core Spotlight 所做的東西是唯一發揮作用的。Core Spotlight 和 Apple 大多數核心框架類似,它大部分是由 C 編寫而成的,而不是用 Objective-C 或者 Swift。因此,我創建了一個 attributeSet 屬性集實例,給定一個內容類型,這個內容類型是框架中預定義好的字符串列表。因為我們在這里聲明的是圖像類型,因此我們想可以在搜索結果中顯示一個實際的圖片。

接著我還需要做其他操作,給定 ID 以及包裝有圖像的 NSData 類型。唯一標識符 (unique ID) 和域標識符 (Domain ID) 都十分有用,域標識符允許你通過分組 (group) 功能做一些非常贊的功能,但是這里我們就只設定一個 “retro-computer”。接著我將屬性集傳遞進去,最后向 CSSearchableIndex 申請一個默認的索引,然后對我們創建的可搜索項建立索引(我們現在只創建了一項)。在模擬器中,我們現在可以看到,在我刪除掉應用并重啟模擬器之后,基于 Core Spotlight 功能的搜索結果看起來就更豐富了,因為它包含了一個圖片和描述信息。

Core Spotlight 和 NSUserActivity 都可以用來相同的功能,但是它們各自都使用著不同的類。很多時候你可能總想將它們一同實現,不過我并不是很確定為什么 Apple 不將它們整合到一個 API 當中,這樣就可以一次性完成這兩個功能了。大家感興趣的話可以去試一試解決這個問題,因為這個項目非常有意思,并且也不是很難。

使用 Core Spotlight 恢復應用狀態(33:12)

為了讓 Core Spotlight 的搜索結果能夠將用戶帶到應用中的合適位置,我在 Step 5 中對主視圖控制器做了一些改動。現在它會檢查傳入的活動對象類型。如果傳遞進來的是 Spotlight 對象的話,,那么活動類型就會被設置為 CSSearchableItemActionType 。這只是一個用來讓你知道結果被點擊的標記而已,因此你可以獲取到這個 Spotlight 項,然后以合適的方式對其進行處理。因此,在這種情況下我會以這種特別的方式獲取 name 信息,這也是我所返回的名字信息。如果不是 Spotlight 搜索項的話,那么獲得的就是 NSUserActivity 搜索項,原有的方法仍然適用。最后,我們就和此前一樣恢復應用狀態即可。

兩全其美(35:36)

為了完整期間,我們現在需要實現一種兩全其美的方式,在 Step 6 中,最好還是把 NSUserActivity 帶回來。我重新啟用了 becomeCurrent() 函數,同樣我還添加了 activity.contentAttributeSet = attributeSet 語句。現在我們在這里為 Core Spotlight 所創建的屬性集就和活動建立起了同樣的聯系,這樣這兩種方式都有著相同的信息了。因此,當 Core Spotlight 或者 NSUserActivity 的信息展示的時候,iOS 會很智能地依據唯一標志符和使用的活動信息來將它們區分開來。這對 Web 也是同樣適用的,我們建議使用網頁的 URL 作為唯一ID。

現在,我們所實現的功能非常令人激動,通過它就可以讓 iOS 系統能夠搜索你應用中的內容了!

問與答(38:24)

問:是否可以在應用中創建一個搜索函數,并利用此函數實現相同索引內容的檢索呢?

Jack:我覺得 Core Spotlight 可以在應用中實現自定義搜索功能。雖然我并沒有詳細了解這方面的內容,但是由于在 OS X 上可以這么做,因此我覺得這應該是可行的。

問:這個用來搜索的數據庫中能放入多少數據?

Jack:當數據太多的時候 iOS 會通知你得,這個時候你就需要刪除一些信息,最好的辦法就是使用我之前提到的域標識符來完成這種操作了。同樣,你通過 Core Spotlight 創建的索引大小會影響整個應用中的數據存儲大小。這里并沒有所謂的理論限制,不過實際上如果你的應用大小比下載時要大得多的話,這可能會有實際限制存在,不過這基本上是很難發生的。因為索引通常情況下是非常小的。

See the discussion on Hacker News .

Sign up to be notified of new videos — we won’t email you for any other reason, ever.

</div>

來自: https://realm.io/cn/news/jack-nutting-search-api-ios/

</span></code></code></code></code></code></code></code></code>

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!