iOS 9 App Search教程
在相當長的一段時間內,iOS 的 Spotlight 都是一個大坑。儘管使用者可以用它來搜尋你的 App,但他們卻無法看到其中的內容——他們真正關心的部分。現在,當使用者想讀取一個 App 中的內容時,他們只能回到 Home 屏一屏一屏翻,找到 App,開啟 App,搜尋他們想要的內容——假設你的App實現了搜尋功能的話。
對於比較老練的使用者,則可能會通過Siri 或者 Spotlight 來開啟你的 App,但無論哪個工具都不能讓使用者尋找“非蘋果官方App”內的內容。也就是說,蘋果在 Spotlight 中可以尋找通訊錄、備忘錄、資訊、郵件以及其它支援尋找功能的App中的內容。使用者只需要點擊搜尋結果就可以直接存取相應的內容。這真是太不公平了!
有時蘋果會將一些有趣的功能保留給自己專用,比如 Spotlight。好訊息是,每當蘋果的開發人員調教好一個功能,覺得已經可以把它放出去的時候,他們就會讓大夥也嘗嘗鮮,比如 iOS 8 中的 App 擴充。
在iOS 9 中,蘋果又放出來一個很酷的功能給我們,第三方開發人員現在可以在Spotlight 中搜尋他們的App 內容了!
在本教程中,你將領略 App Search 的威力,並學會如何將它整合到你自己的App 中。
App Search API
在iOS 9 中,App Search由三個部分組成。每一部分根據不同的目的分成獨立的 API,但它們也能和其它部分一起使用:
NSUserActivity Core Spotlight Web markupNSUserActivity
在App Search 中,使用了NSUserActivity,這是一個靈活小巧功能,在iOS 8 Handoff 中就使用到了NSUserActivity 。
在iOS 9 中,NSUserActiviy增加一些新的屬性以支援 App Search。從理論上講,如果一個任務能夠轉變成一個 NSUserActivity 並轉交給其它裝置,它也能轉換為一個搜尋項並在同一個裝置上繼續處理。這就有可能對App 上的活動、狀態和導航點進行索引,這樣使用者才能在 Spotlight 中對其進行搜尋。
例如,一個旅遊類App 可能會將使用者查看過的酒店進行索引,而一個新聞類App 會將使用者瀏覽過的文章進行索引。
注意:本教程不涉及 Handoff,我們只會討論當一個內容被瀏覽後如何建立可搜尋的內容。如果你想熟悉瞭解 Handoff 的內容,請閱讀 Getting Started with Handoff 教程。
Core Spotlight
第二個同時也是App Search 中最“常用到的”概念就是 Core Spotlight,它是儲存類 App 諸如郵件、備忘錄用於索引內容的東西。它既可以允許使用者搜尋之前訪問過的內容,也可以用它來一次性構建一個巨大的可搜尋內容的集合。
你可以將Core Spotlight 看成是一個專門用於搜尋的資料庫。它提供了對添加搜尋索引中的內容的細粒度的控制,例如這些內容是什麼、什麼時候添加的以及如何添加到搜尋索引中的。你可以檢索任何類型的內容,包括檔案、視頻、訊息等等,還可以更新和移除搜尋索引中的條目。
Core Spotlight 為全面搜尋 App 內部內容提供了一種最好的方式。
本教程關注於使用前面提及的 NSUserActivity 對象擷取 Spotlight 搜尋結果。本教程的完整版位於《iOS 9 Tutorials》中,其中介紹了如何通過Core Spotlight 全面檢索你的內容。
Web markup
App Search的第三個方面是 Web Markup,這個功能允許App 將它們的內容鏡像到一個Web網站上。比較好的例子如 Amazon,你可以搜尋它上面成千上萬的在售產品,甚至是raywenderlich.com上的產品。在web 內容上使用了標準的標籤進行標記,你可以將App 內容顯示在 Spotlight 和 Safari的搜尋結果中,甚至可以直接連結到你的App。
本教程不涉及 Web Markup,你可以在《iOS 9 by Tutorials》第三章“Your App On The Web”中學習這部分內容。
開始
你將學習的樣本程式叫做 Colleagues,它類比一個公司通訊錄。它可以將你的同時添加到你的連絡人中,而不是直接給你一個同事的目錄。為了簡單起見,它使用的是本機資料庫,由一個檔案夾(存放頭像圖片)和一個 JSON檔案(包含了所有公司職員資訊)組成。在生產環境中,你應該使用一個網路組件從 Web 上抓取這些資料。作為教程,JSON 檔案就足夠了。下載並開啟初始項目,不需要做任何事情,直接編譯運行。
你會看到一張職員列表。這是一個小型創業公司,只有25個職員的規模。選擇 Brent Reid,可以查看這個職員的資訊。你同時還可以看到 Brent Reid 所在的部門的其他人的列表。那是 App 的一個擴充功能——其實非常簡單!
搜尋功能將讓 App 增色不少。但是現在,你甚至無法在 App 搜尋。你不用在 App 中增加搜尋功能,相反,你可以用 Spotlight 從 App 外部增加一個搜尋功能。
樣本項目
花點時間來熟悉一下樣本項目的代碼。項目中存在兩個 Target,一個是 Colleagues,即 App 自身;一個是 EmployeeKit,負責和職員資料庫進行互動。
在 Xcode 中,展開 EmployeeKit 檔案夾,開啟 Employee.swift。這是職員資訊的模型類,定義了一系列相關屬性。Employee 對象使用一個 JSON 對象進行執行個體化,後者來自於 Database 檔案夾下的 employees.json 檔案。
然後開啟 EmployeeService.swift。在檔案頭部聲明了一個擴充,擴充中有一個 destroyEmployeeIndexing()方法,這個方法用TODO標記進行註明。你將在稍後實現這個方法。這個方法負責銷毀所有顯示過的索引。
在 EmployeeKit 這個 Target 中有許多內容,但都和 App Search 毫無關聯,因此我們就不多說了。當然,你可以花時間看一看。
開啟Colleagues 檔案夾下的 AppDelegate.swift。注意只有一個方法在裡邊:
application(_:didFinishLaunchingWithOptions:)。這個方法判斷Setting.searchIndexingPreference 是否設定為 .Disabled,如果是,則將所有存在的搜尋索引刪除。
除了知道有這麼一個設定項存在外,你並不需要做任何事情。你可以通過 iOS 的設定程式中的 Colleagues 來修改這個設定。
參觀到此結束。接下來你需要修改 View Controller 中的代碼。
搜尋曾經瀏覽過的記錄
實現App Search時,NSUserActivity 總是第一個要實現的,因為:
它最簡單。建立一個 NSUserActivity 執行個體就如同設定幾個屬性那麼簡單。
當你用 NSUserActivity 表示使用者活動時,iOS 會對內容進行排序,以便搜尋結果對經常被訪問的內容進行優先處理。
它和實現 Handoff 很像。
現在,讓我們來看看實現 NSUserActivity 到底有多簡單!
實現 NSUserActivity
選中 EmployeeKit 檔案夾,依次選擇 File \ New \ File…,然後選擇 iOS\ Source \ Swift File 模板,再點擊 Next。將檔案命名為 EmployeeSearch.swift,並確保其 Target 為 EmployeeKit。
在這個檔案中,首先匯入 CoreSpotlight:
import CoreSpotlight
然後定義一個 Employee 的擴充:
extension Employee { public static let domainIdentifier = "com.raywenderlich.colleagues.employee"}
反網域名稱字串將用於唯一標識 NSUserActivity 所屬的一類活動類型。接著,在domainIdentifier 之後增加一個計算屬性:
public var userActivityUserInfo: [NSObject: AnyObject] { return ["id": objectId]}
這個字典用於 NSUserAcitivity 唯一標識某個活動(Activity)。然後再添加一個計算屬性,名為 userActivity:
public var userActivity: NSUserActivity { let activity = NSUserActivity(activityType: Employee.domainIdentifier) activity.title = name activity.userInfo = userActivityUserInfo activity.keywords = [email, department] return activity}
這個屬性用於很方便地根據一個 Employee 建立一個 NSUserActivity 執行個體。它建立了一個 NSUserActivity 對象,並用對以下屬性進行了賦值:
activityType:活動所屬的類型。你會在後面用它來識別 iOS 傳遞給你的NSUserActivity執行個體。蘋果建議該值採用反網域名稱命名規則。
title:活動的名字——這將用於在搜尋結果中作為主要名顯示。
userInfo:一個字典,用於存放你想傳遞的任意資料。當你的App 收到一個活動時——比如使用者從 Spotlight 點擊了一個搜尋結果,你就可以擷取這個字典。你將在這個字典中存放同事的唯一 ID,這樣 App 開啟後就能顯示正確的同事資料。
keywords:一個本地化的關鍵字列表,用於作為搜尋索引鍵。
然後,我們將使用剛才定義的 userActivity 屬性去搜尋同事記錄。因為這些代碼位於 EmployeeKit 架構中,我們需要編譯架構才能在 Colleagues App 中使用它們。
按 Command+B,編譯項目。
開啟 EmployeeViewController.swift,在viewDidLoad()方法最後加入代碼:
let activity = employee.userActivityswitch Setting.searchIndexingPreference {case .Disabled: activity.eligibleForSearch = falsecase .ViewedRecords: activity.eligibleForSearch = true}userActivity = activity
上述代碼讀取 userActivity 屬性——這個屬性是我們剛才通過定義 Employee 擴充時添加的。然後檢查 App 的搜尋設定。
如果搜尋被禁用,將 activty 標記為不可用於搜尋。如果該設定為 ViewedRecords,則將 activity 標記為能夠用於搜尋。
最後,將 View Controller 的 userActivity 屬性設定為 employee 的 userActivity。
注意:View Controller 的 userActivity 屬性繼承自 UIResponder 。這個屬性是蘋果為了支援 Handoff 而增加到 iOS 8 中的。
最後還應該覆蓋 updateUserActivityState() 方法。這樣,當某個搜尋結果被選擇時,你才可以獲得所需要的資料。
在 viewDidLoad() 方法後增加這個方法:
override func updateUserActivityState(activity: NSUserActivity) { activity.addUserInfoEntriesFromDictionary( employee.userActivityUserInfo)}
在 UIResponder 的生命週期中,系統會多次調用這個方法,你應該在這個方法中保持更新 activity。在我們的例子裡,你只需要將包含有 employee的 objectId 的 userActivityUserInfo 字典傳遞給 activity。
好了!現在,在搜尋設定被開啟的情況下,每當你瀏覽了一個同事,瀏覽曆史將被記下並可用於搜尋。
在模擬器或裝置上,開啟設定程式,找到 Colleagues。將 Indexing 設定改成 Viewed Records。
現在,編譯運行程式,然後選擇 Brent Reid。
OK,看起來沒有什麼新奇的事情發生,但在你不知不覺中,Brent 的活動已經被加到搜尋索引中了。回到 Home 螢幕(shift+command+H),通過下拉螢幕或者向右划動螢幕,開啟 Spotlight。在搜尋欄輸入 brent reid 。
“Brent Reid”顯示出來了!如果你沒看見,可能需要向下滾動列表。如果你點擊這個Brend Reid,它將移動到列表上部,以便下次你可以搜尋同一個關鍵字。
雖然到現在為止結果還蠻不錯,但這個搜尋結果卻是有點索然無味了。
除了顯示一個名字外,我們還能幹什嗎?現在就讓我們徹底進入 Core Spotlight 的殿堂探索一番。
在搜尋結果中顯示更多資訊
NSUserActivity 有一個 contentAttributeSet 屬性。這個屬性的類型是 CSSearchableItemAttributeSet,它允許你用一系列屬性來描述你的內容。查看 CSSearchableItemAttributeSet 類參考,你可以發現很多利用這些屬性來描述內容的方法。
是我們需要的搜尋結果,每個部分都分別標出了所用的屬性名稱:
前面已經設定過 NSUserActivity 的 title 屬性,這個屬性正如你所看到的。其它3個屬性,thumbnailData、supportsPhoneCall 和 contentDescription 全部都是通過 CSSearchableItemAttributeSet 來設定的。
開啟 EmployeeSearch.swift,在檔案頭部,匯入 MobileCoreServices:
import MobileCoreServices
MobileCoreServices 是必須的,因為在我們建立 CSSearchableItemAttributeSet 對象時需要用到其中定義的一個常量。你已經匯入過 CoreSpotlight了,這個架構也是必須的,它的所有 API 都使用了 CS 作為首碼。
仍然在 EmployeeSearch.swift中,在 Employee 擴充中添加新的計算屬性:
public var attributeSet: CSSearchableItemAttributeSet { let attributeSet = CSSearchableItemAttributeSet( itemContentType: kUTTypeContact as String) attributeSet.title = name attributeSet.contentDescription = "\(department), \(title)\n\(phone)" attributeSet.thumbnailData = UIImageJPEGRepresentation( loadPicture(), 0.9) attributeSet.supportsPhoneCall = true attributeSet.phoneNumbers = [phone] attributeSet.emailAddresses = [email] attributeSet.keywords = skills return attributeSet}
初始化 CSSearchableItemAttributeSet 時,需要提供一個 itemContentType 參數,我們傳遞了一個 kUTTypeContact 進去(該常量在 MobileCoreServices 架構中定義,關於該常量,請閱讀蘋果的 UTType 參考)。
attributeSet 中包含了一些與當前 employee 搜尋時用到的相關資料:title 來自於 NSUserActivity 的 title,contentDescription 包括了這個同事的部門、稱謂和電話號碼等資訊,而 thumbnailData 則調用 loadPicture() 方法結果並轉換為 NSData。
要顯示”打電話“按鈕,我們必須將 supportsPhoneCall 設定為true,並給 phoneNumbers 屬性賦一個數組。最後,我們設定了 email 地址,並將同事的 skills (技能)作為 keyword 關鍵字。
現在所有的資料都準備好了,Core Spotlight 在搜尋時會檢索這些資料並添加到搜尋結果中。這樣,使用者就可以搜尋同事的姓名、部門、稱謂、電話號碼、email甚至是技能。
仍然是 EmployeeSearch.swift,在返回 userActivity 前面添加以下語句:
activity.contentAttributeSet = attributeSet
這句代碼告訴 NSUserActivity 使用這些資訊作為 contentAttributeSet屬性的值。
編譯運行。查看 Brent Reid 的個人資訊以便索引生效。回到 Home 螢幕,拉出 Spotlight,搜尋 brent reid。如果你先前的搜尋結果仍然存在,你只需要清除並重新搜尋。
噢,你是不是很奇怪實現的代碼太少了?
好了!現在 Spotlight 能夠如我們所想的一樣搜尋同事了。不過,似乎我們還是遺漏了點什麼…當你嘗試通過搜尋結果開啟 App 時,什麼也不會發生。
開啟搜尋結果
理想的使用者體驗是直接開啟 App 並顯示相關的內容。事實上——這個是一個要求——蘋果會將能夠啟動並顯示有用的資訊的App的排在搜尋結果的前列。
通過將一個 activityType 和一個 userInfo 對象賦給 NSUserActivity 對象,你已經在上一節中為後續的工作做了鋪墊。
開啟 AppDelegate.swift,在application(_:didFinishLaunchingWithOptions:) 方法下面,添加
一個application(_:continueUserActivity:restorationHandler:) 方法:
func application(application: UIApplication, continueUserActivity userActivity: NSUserActivity, restorationHandler: ([AnyObject]?) -> Void) -> Bool { return true}
當使用者選擇了一個搜尋結果時,這個方法會被調用——這個方法也會被Handoff 用來接收其他裝置傳來的活動。
在這個方法返回 true 之前,加入以下語句:
guard userActivity.activityType == Employee.domainIdentifier, let objectId = userActivity.userInfo?["id"] as? String else { return false}
guard 語句檢查 activityType 是否是我們希望的類型(用於處理 Employee 的活動),然後從 userInfo 中擷取 objectId。如果這兩個條件中有一個不滿足則返回 false,通知系統該活動不會被處理。
接著,在 guard 語句後,將 return true 語句替換為:
if let nav = window?.rootViewController as? UINavigationController, listVC = nav.viewControllers.first as? EmployeeListViewController, employee = EmployeeService().employeeWithObjectId(objectId) { nav.popToRootViewControllerAnimated(false) let employeeViewController = listVC .storyboard? .instantiateViewControllerWithIdentifier("EmployeeView") as! EmployeeViewController employeeViewController.employee = employee nav.pushViewController(employeeViewController, animated: false) return true}return false
獲得 id 之後,你的目標就是用EmployeeViewController 顯示匹配的同事資訊。
上述代碼稍微有點亂,但你可以想象一下 App 的設計。App 中有兩個 View Controller,一個是同事的列表,另一個是則顯示同事的詳細資料。上述代碼先將導航控制器的視圖控制器堆棧彈回到列表介面,然後push 一個該同事細節視窗。
如果因為某種原因視圖無法呈現,方法會返回一個false。
OK,編譯和運行!從同時列表中選擇 Cary Iowa,然後回到 Home 屏。調出 Spotlight 搜尋 Brent Reid。找到結果後,點擊它。App 會開啟,並且可以看到 Cary 的詳情介面迅速地過渡到了 Bent 的詳情介面。幹得不錯!
從搜尋索引中刪除條目
回到 App 的話題上來。想象一下,在某個狂風暴雨的一天,一個同事因為將老闆用膠帶綁在牆上而被解僱。顯然,你是無論如何都不想和這個人有任何關係了,因此你必須將他和其他離開公司的人一起從 Colleagues 的搜尋索引中刪除。
由於只是一個樣本App,你可以在 App 的索引設定關閉的前提下將整個索引刪除。
開啟EmployeeService.swift 在檔案頭部添加匯入語句:
import CoreSpotlight
找到 destoryEmployeeIndexing(),將 TODO 注釋替換為:
CSSearchableIndex .defaultSearchableIndex() .deleteAllSearchableItemsWithCompletionHandler { error in if let error = error { print("Error deleting searching employee items: \(error)") } else { print("Employees indexing deleted.") }}
這個無參的方法將刪除整個App的索引資料庫。Good!
現在可以來測試一下。通過下列步驟來測試是否索引一如我們希望的那樣已被刪除:
編譯運行程式。
用 Xcode 終止程式。
在模擬器或者裝置中,開啟 設定 \ Colleagues,將 Indexing 設定為 Viewed Records。
再次開啟 App,選擇一個新的同事,讓索引生效。
回到 Home 屏,調出 Spotlight。
搜尋瀏覽過的同事,等待索引項目出現。
回到 設定 \ Colleagues,將 Indexing 設定為關。
退出 App。
重新開啟 App。這將清除搜尋索引。
回到 Home 屏,調出 Spotlight。
搜尋瀏覽過的同事,你會發現沒有和 Colleagues App 有關的搜尋結果。
呵呵,刪除整個搜尋索引實在太容易了。但如果你想只刪除某個單獨的記錄呢?幸運的是——有兩個 API 能夠讓你更精確地刪除想刪的記錄:
deleteSearchableItemsWithDomainIdentifiers(_:completionHandler:) 方法允許你刪除整個 domain ID 相同的一組索引。
deleteSearchableItemsWithIdentifiers(_:completionHandler:) 方法允許你通過唯一ID 指定要刪除哪條記錄。
也就是說,如果你所索引的記錄具有多種類型的話,全域識別碼 (在同一個 App 組中)必須唯一。
注意:如果你不能保證跨類型ID 是唯一的,比如你的 ID 是通過資料庫中的自增長類型獲得的,則你可以採取一種簡單辦法,即在記錄 ID 前面加上一個類型首碼。例如,如果你有一個連絡人記錄的 ID 為 123,一個訂單記錄的 ID 也是 123,則可以將它們的唯一 ID 設定為 contact.123 和 order.123。
如果你在運行過程中遇到任何問題,你可以從這裡下載到最終完成的項目。
接下來做什嗎?
這篇 iOS 9 App Search 教程介紹了在 iOS 9 中使用 User Activity 搜尋 App 內部內容的簡單但強大的方法。搜尋的內容從來不會受到限制——你可以用這種方法搜尋 App 中的導航點。
想象一下,一個 CRM App,它擁有許多視窗,比如連絡人、訂單和任務。通過 User Activity,使用者隨時可以到達這些視窗,使用者可以搜尋訂單,然後直接跳到 App 的某個訂單介面。這個功能太有用了,尤其是你的 App 有很多層級的導航時。
有許多獨特的方法將內容推給你的使用者。想突破沙箱的限制,就要教會使用者使用這個強大的功能。
這個教程是《iOS 9 by Tutorials》第2章的精簡版。如果你想學慣用 Core Spotlight 檢索大資料集,或者學習 iOS 9 的 Web Content,請閱讀這本書!