在相當長的一段時間內,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 中。
在iOS 9 中,App Search由三個部分組成。每一部分根據不同的目的分成獨立的 API,但它們也能和其它部分一起使用:
NSUserActivity Core Spotlight Web markup在App Search 中,使用了NSUserActivity,這是一個靈活小巧功能,在iOS 8 Handoff 中就使用到了NSUserActivity 。
在iOS 9 中,NSUserActiviy增加一些新的屬性以支持 App Search。從理論上講,如果一個任務能夠轉變成一個 NSUserActivity 並轉交給其它設備,它也能轉換為一個搜索項並在同一個設備上繼續處理。這就有可能對App 上的活動、狀態和導航點進行索引,這樣用戶才能在 Spotlight 中對其進行搜索。
例如,一個旅游類App 可能會將用戶查看過的酒店進行索引,而一個新聞類App 會將用戶浏覽過的文章進行索引。
注意:本教程不涉及 Handoff,我們只會討論當一個內容被浏覽後如何創建可搜索的內容。如果你想熟悉了解 Handoff 的內容,請閱讀 Getting Started with Handoff 教程。
第二個同時也是App Search 中最“常用到的”概念就是 Core Spotlight,它是存儲類 App 諸如郵件、備忘錄用於索引內容的東西。它既可以允許用戶搜索之前訪問過的內容,也可以用它來一次性構建一個巨大的可搜索內容的集合。
你可以將Core Spotlight 看成是一個專門用於搜索的數據庫。它提供了對添加搜索索引中的內容的細粒度的控制,例如這些內容是什麼、什麼時候添加的以及如何添加到搜索索引中的。你可以檢索任何類型的內容,包括文件、視頻、消息等等,還可以更新和移除搜索索引中的條目。
Core Spotlight 為全面搜索 App 內部內容提供了一種最好的方式。
本教程關注於使用前面提及的 NSUserActivity 對象獲取 Spotlight 搜索結果。本教程的完整版位於《iOS 9 Tutorials》中,其中介紹了如何通過Core Spotlight 全面檢索你的內容。
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 到底有多簡單!
選中 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.userActivity
switch Setting.searchIndexingPreference {
case .Disabled:
activity.eligibleForSearch = false
case .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)
(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 指定要刪除哪條記錄。
也就是說,如果你所索引的記錄具有多種類型的話,全局 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 App Search Tutorial: Introduction to App Search 原文作者 : Chris Wagner 譯文出自 : 開發技術前線 www.devtf.cn 譯者 : kmyhy
Ray 注:本文作為《iOS 9 Feast》中的一部分,節略自 《iOS 9 Tutorials》其中一章——通過本文,您可對全書內容窺見一斑。祝您閱讀愉快!