如何使用iOS 9的Core Spotlight框架
來自: http://www.cocoachina.com/ios/20160128/15163.html
-
本文由CocoaChina譯者KingOfOnePiece( 博客 )翻譯
-
作者: GABRIEL THEODOROPOULOS 校對:hyhSuper
iOS每一次版本的更新,都會給全球的開發工作者帶來新的“知識點”和對現有技術進行的改進。顯然,iOS的最新版本iOS 9不僅延續了這一傳統,還公布了新的框架和API,開發者可使用新增的框架和API讓自己的應用表現的更出色。其中之一就是Core Spotlight框架,它包含了一些優秀的API,有待開發者深入探究。
Core Spotlight (CS) 框架是Search API的一部分,它增加了開發者的應用的曝光率,使用戶更容易發現和訪問APP,但是這些API在iOS9之前的版本中是不可用的。這些Search API拉近了用戶和app的距離,用戶可以通過這種新的方法迅速的找到app,而app也可以迅速的對用戶的操作做出反應。除了Core Spotlight之外,iOS 9還包含以下新的search功能(僅供參考):
1、NSUserActivity這個類中新的方法和屬性(該類負責存儲app關閉時的狀態,以便之后恢復app的狀態。)
2、在設備上使用web標記,以便搜索到web中的內容。
3、能通過web內容中的鏈接直接啟動應用程序的通用鏈接。
本文不會對以上三點做詳細介紹,只詳細講解Core Spotlight框架。在講解之前,先了解以下Core Spotlight到底是什么?
如上圖所示: Core Spotling框架 可以讓用戶通過Spotlight搜索到app中的數據,并將app相關內容和系統搜索返回的結果一起展示出來。也就是說用戶可以與應用程序的其他相關結果進行交互,當點擊其中一項搜索結果時并不僅僅自動啟動APP,開發者也可以讓用戶選擇與數據最相關的內容。
從開發者的角度來說,集成Core Spotlight框架并使用它提供的API并不是一件難事。學過這教程之后,你將會發現僅僅需要幾行代碼就可以實現這個功能。集成的核心問題在于開發者必須把APP的數據以特定的格式加入到iOS系統的索引中。
由于本教程專注于Core Spotlight框架的使用,所以我不打算深入研究這個框架。
關于Demo App
和往常一樣,我們打算通過一個示例應用來研究探索Spotlight的細節。在這個demo中,我們將往應用程序中寫入一組數據,并且這些數據都是可以通過真機或模擬器的Spotlight功能被搜索到的。雖然實現Spotlight功能搜索是重點,但是還是有必要介紹一下關于demo app 的更多細節。
我們的示例應用要達到的效果是展示一些電影及其相關的信息,例如電影概要、導演、明星以及影評等級等。所有的電影數據將會放在列表中展示,并且當點擊到對應的行時,所選擇的電影會在一個新的視圖控制器中展示出電影的詳情。這些功能和數據作為我們挖掘Spotlight的API而言已經夠用了。我們的數據來源是International Movie Database (IMDB),我們從這個網站獲得數據示例。
通過看下面的動畫示例,你可以先看一下演示程序的效果:
在本教程中,我們有兩個目的:最重要的是讓這個應用內的所有電影方面的數據都可以被Spotlight搜索到。當用戶通過關鍵字搜索時,應用程序內關于電影的數據將會展現給用戶。設置這些關鍵字是我們接下來工作的一部分,我們必須重新定義它們,使之符合規則。
點擊搜索結果會打開應用程序,之后我們要實現第二個目標。如果我們沒有做任何的處理,那么將會加載包含有電影數據列表的默認視圖控制器展示給用戶。然而,假如我們考慮到用戶體驗的話,這么處理并不是很好。好的方案是,我們的APP應該展示Spotlight選中的電影的詳情。簡言之,我們不僅要讓應用程序內的電影數據能夠通過Spotlight被搜索到,而且還要在用戶點擊搜索結果時,展示關聯的電影詳情頁。接下來的示例動畫會講解的更清楚:
為了節約時間,你可以在這里下載工程,然后開始我們的工作。在工程里面你會發現如下所示的內容:
-
界面部分的組件已經搭建完成,并且包含所有必要的IBOutlet屬性。
-
最小化的列表視圖。
-
所有的電影數據都存放在以plist為后綴的文件中。此外圖片文件是和電影一一對應的(一共有5部)。
假使你想知道預先準備的文件中包含了哪些對應電影的數據,那么你可以通過這個截圖示例看到所有包含的內容。
先看一下Core Spotlight API的具體信息,我們會處理下面兩個任務:
1、在列表中加載和展示電影數據。
2、在選中電影數據列表的某一行時會跳轉到對應的電影詳情界面。
啟動項目沒有實現上邊的內容,盡管那樣么做會讓你們快速進到我們的話題,原因很簡單:我很確信通過演示應用程序的核心功能和數據樣本,你會很容易地了解到將要被Spotlight搜索到的具體數據。但是別擔心,因為所有準備工作花費的時間很少,并且很快就可完成。
加載和展示示例數據
假如此時你已經下載好了起始工程,并且已經看過了電影數據屬性列表,那么就讓我們開始Coding吧。在MoviesData.plist這個文件夾下,你可以看到一共有5條數據,這些數據是從IMDB網站上隨機選取的他們對應5個電影示例。我們第一步是從plist文件中加載數據到一個數組中,然后在列表中展示他們。
廢話少說,直接上代碼。打開ViewController.swift這個文件,并且在這個類聲明屬性的地方這么寫:
var moviesInfo: NSMutableArray!
所有的電影模型數據都會被加載到moviesInfo可變數組中,單個電影模型的數據將會以鍵值對的方式保存在字典中,并且他們和文件中的屬性列表相匹配。
現在讓我們編寫一個加載數據的自定義函數。接下來你會看到,我們只是確保屬性列表文件是否存在,如果存在,我們就初始化數組的文件內容:
func loadMoviesInfo() { if let path = NSBundle.mainBundle().pathForResource("MoviesData", ofType: "plist") { moviesInfo = NSMutableArray(contentsOfFile: path) } }
接下來我們需要在viewDidLoad()函數調用loadMoviesInfo()函數。只是為了確保你在調用configureTableView()函數前正確調用此函數,下面展示的是代碼片段:
override func viewDidLoad() { super.viewDidLoad() // Load the movies data from the file. loadMoviesInfo() configureTableView() navigationItem.title = "Movies" }
要注意的是,我們只需把文件內容加載到viewDidLoad()函數,而不是為了創建上述的loadMoviesInfo()函數,但是作為喜歡代碼整潔的人,即便是這么小的事情,都會選擇更好的方式來實現。
應用程序每次啟動時都會加載這些電影數據,我們可以繼續修改當前視圖列表,讓它顯示電影信息。需要處理的只有這么多:根據電影數據信息定義列表的行數,然后在表視圖的單元格中展示正確的電影數據。
從列表視圖的行數開始,顯然列表視圖的行數等于電影的數目。然而,我們不應忘記,要確保列表視圖中有電影數據展示出來,否則當文件內容不被加載到數組中時就會造成應用程序的崩潰。
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { if moviesInfo != nil { return moviesInfo.count } return 0 }
最后,讓我們展示電影數據。為了達到我們需要的目標,在開始準備的工程中你可以找到UITableViewCell的子類MovieSummaryCell,和代表一個電影單元格的.xib文件:
用這樣的一個單元格來展示每個電影的圖片、標題、部分的描述信息和電影評級。所有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文件,根據下面的代碼片段更新表視圖的函數:
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這個字典不是必需的,但是它讓上面的簡單代碼編寫的更加容易。
這時你可以運行一次你的應用程序,并且可以看到電影的細節被展示在表視圖。到目前為止,我們所做的這些是大家所熟知的,所以直接到第二個步驟:顯示所選電影的細節。
數據細節展示
在MovieDetailsViewController這個類中,我們將要展示在ViewController類中tableView上選中的電影的細節。所有的界面構建已經完成,現在我們必須做兩件事:把包含電影信息的字典從ViewController類傳到MovieDetailsViewController類中,然后從字典取值賦值給這個類中相應的UI控件,這些控件的IBOutlet屬性都已經被聲明并且已經正確的連接到單個UI組件上。
所以,說到字典,讓我們在MovieDetailsViewController類中做下面的聲明:
var movieInfo: [String: String]!
回到ViewController.swift文件,讓我們看一下點擊表視圖的行時需要做的工作有哪些。當點擊列表的一行時,我們需要知道點擊行的索引,以便于從moviesInfo數組選擇合適的字典,然后在視圖跳轉的時候把字典傳遞到下一個視圖控制器。從tableview的委托方法中獲取行索引是很容易的,但是我們需要一個自定義的屬性來存儲它,因此在ViewController類的頂部我們需要這么聲明:
var selectedMovieIndex: Int!
然后,我們需要用下面的方式處理tableView中選中row的索引:
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) { selectedMovieIndex = indexPath.row performSegueWithIdentifier("idSegueShowMovieDetails", sender: self) }
有兩個簡單的事情需要處理:第一是把點擊的行的索引存儲在我們自定義的selectedMovieIndex屬性中,然后執行跳轉到展示電影詳情的界面。然而,這是不夠的,因為我們還未從moviesInfo數組中選擇相應的包含電影信息的字典,并把它傳遞到MovieDetailsViewController這個類中。我們該怎么做呢?重寫prepareForSegue:sender:函數,并實現我剛剛描述的功能。下面是代碼,請看:
override 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] } } }
很簡單,我們只通過destinationViewController的segue屬性獲得MovieDetailsViewController實例,然后我們將包含電影信息的字典值賦給我們這部分開始時聲明的movieInfo屬性。
現在,再次打開MovieDetailsViewController.swift文件,我們將會在這里定義一個自定義的函數。在它里面,我們將會從movieInfo字典中取值賦給那些相應的控件,那么這部分的工作到這里就就結束了。下面的代碼是一個簡單的實現,所以不再做進一步的探討:
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添加app的索引數據
通過使用iOS9中提供的Core Spotlight 框架,使得任何一款應用都可以通過Spotlight功能被搜索到。在Spotlight上通過用戶的搜索行為找到app的關鍵在于使用Core Spotlight API索引到我們應用的數據。但是既不是我們的app也是不是CS API接口決定應該設置什么類型的數據。所以我們需要提供特定的數據格式給 API接口。
再具體點來說,就是我們想通過Spotlight功能搜索到得數據必須封裝成CSSearchableItem類型的對象,然后一起組裝到數組中并把這個數組傳遞給CS API 中供它索引。單個的CSSearchableItem對象包含一系列的屬性,這些屬性使得iOS系統可搜索對象的細節更加清楚。像在搜索時應該展示哪些數據片段(例如:電影名稱、圖片和影視簡介等),哪些app的數據關鍵字需要在Spotlight上展示。CSSearchableItemAttributeSet 類對象為 CSSearchableItem對象提了許多屬性,只需為我們所需要的屬性賦值就好了。查看 官方文檔 ,你可以找到所有支持的屬性。
最后一步通常要做的是為Spotlight功能添加索引數據。一般情況下,實現流程如下步驟所示(包含添加索引):
1、給每一條數據設置屬性,例如電影模型數據(設置CSSearchableItemAttributeSet對象)。
2、使用上一步的屬性值(CSSearchableItem對象),為每一條數據初始化搜索項并賦值。
3、收集所有的可搜索項放到一個數組中。
4、使用上個步驟的數組作為Spotlight的索引數據。
我們將按照上面的步驟一個一個的實現,為了達到目標,我們將會在ViewController.swift這個文件內創建一個叫做setupSearchableContent()的函數。在這部分功能實現結束之際,你會發現實現讓你的數據可以被搜索到的功能一點也不難。不過,我不會一次性給你所有的實現代碼;相反,為了你更容易理解,我打算把代碼分為片段。別擔心,代碼量并不是那么多。
在我們開始實現新功能之前,我們必須先導入兩個框架:
import CoreSpotlight import MobileCoreServices
我們定義一個新的方法,同時在方法中聲明一個用來存放我們搜集到的搜索項的數組:
func setupSearchableContent() { var searchableItems = [CSSearchableItem]() }
現在在一個for循環中訪問每一個包含電影信息的字典:
func setupSearchableContent() { var searchableItems = [CSSearchableItem]() for i in 0...(moviesInfo.count - 1) { let movie = moviesInfo[i] as! [String: String] } }
我們將為每一部電影創建一個CSSearchableItemAttributeSet對象,然后我們將為搜索項數據設置屬性,這些搜索項數據將會在用戶使用Spotlight搜索時作為搜索結果展示出來。在demo App中,我們將把電影的標題、簡介和圖片作為數據展示給用戶。
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) // Set the title. searchableItemAttributeSet.title = movie["Title"]! // Set the movie image. let imagePathParts = movie["Image"]!.componentsSeparatedByString(".") searchableItemAttributeSet.thumbnailURL = NSBundle.mainBundle().URLForResource(imagePathParts[0], withExtension: imagePathParts[1]) // Set the description. searchableItemAttributeSet.contentDescription = movie["Description"]! } }
注意上面的片段代碼中我們是如何設置電影數據模型中圖片屬性的。有兩種實現方法:一種是為imge制定一個URL鏈接,另一種是為image提供一個NSData對象。最簡單的方法是為每一個電影圖片文件提供一個URL,因為我們知道它們就在程序應用包里面。然而,這樣做就需要我們把每個圖片文件名分割成實際名稱和擴展類型,因此我們用String類的 componentsSeparatedByString:方法將這兩個值分開。剩下的代碼就容易理解了。
現在是時候為app在Spotlight上設置我們想要的data的關鍵字了。指定關鍵字之前要認真考慮,因為你的決定對用戶和App在Spotlight上的曝光率起著最終的決定性作用。在demo App中我們把電影所屬的類別和演員設置成關鍵字。
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數組中以方便及時訪問。然后用一個for循環在keywords數組中添加分類的關鍵字。對電影數據模型的明星屬性使用相同的步驟,也就是說我們再次把包好所有明星名字的字符串分割成單獨的值,然后把他們存入keywords數組中。
上面的片段代碼中最后一句代碼是很重要的,我們為每一個電影數據模型設置了關鍵字屬性。忘記了這一點,當用Spotlight搜索時將不會出現任何關于我們app的結果。
我們已經為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) } }
上述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搜索電影。運行app,然后退出app使用Spotlight搜索上面我們設置的關鍵字。結果將會展現到你眼前,通過點擊搜索結果中的任意項,這個app將自動啟動。
實現目標
當用Spotlight搜索時,能夠從應用程序中搜索到電影數據是會給人深刻的印象,但是我們還可以比這做的更好。現在,當選擇一個搜索結果時,就會啟動應用程序,并且展示出ViewController這個界面,但是我們目標是直接進到展示電影細節界面并可以直接看到所選擇電影的信息。
盡管這聽起來比較困難,或者難以實現,但是最終你會發現它很簡單。在這個指定的demo app中,根據我們現有的數據展示所選電影項的詳細信息會更簡單。
這里主要的工作是重寫UIKit中的一個名字叫做restoreUserActivityState:的函數,并處理Spotlight上選中的結果項。我們最終的目標是從搜索項的標識符(如果你還記得話,我們在上一個部分動態的創建了標識符)中提取出在moviesInfoarray數組中的電影模型的索引值,然后用它傳遞一個正確的電影數據字典并展示MovieDetailsViewController視圖。
restoreUserActivityState:函數的參數是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 type是否和CSSearchableItemActionType匹配。老實說,在示例程序中這個并不重要,但是如果你在你的應用程序中處理多個NSUserActivity對象的情況下,有些事是不應該忘記的做的(例如,Handoff feature是ios8首次提出來的,并且它充分使用了NSUserActivityclass類)。在useInfo字典中identifer對象是一個字符串。一旦我們得到它,我們將基于點符號截取這個字符串,我們從截取的字符串數組中取出最后一個對象就是選中的電影數據模型在電影數據模型數組中的索引值。剩下的就工作就比較簡單:將這個索引值復制給selectedMovieIndex屬性,然后執行跳轉。我們之前的實現將會完成剩余的工作。
現在切換到AppDelegate.swift文件。我們需要實現一個現在還未實現的代理函數。每次點擊在Spotlight上搜索的一個結果時,那個函數就會調用一次,我們現在的任務是調用我們上面已經實現的函數,傳遞userActivity對象,請看下面的實現代碼:
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對開發者來說看起來相當的吸引人,它們使得用戶更容易發現和訪問APP。在這個教程中我們實現了所有Spotlight搜索App數據的相關操作,包括方便用戶在Spotlight上搜索時App數據索引,以及如何把選中的結果項通過app處理后的數據展示給用戶。在你的應用程序實現這樣的功能肯定會提升用戶體驗,所以你應該在你當前和未來的項目認真考慮使用它。又到了結束的時候,我真希望你能發現這篇文章的有用之處!
作為參考,你 從GitHub上可以下載完整的工 程。