Swift 程式語言

如何利用 Core Spotlight 框架增加 iOS Apps 的曝光率

如何利用 Core Spotlight 框架增加 iOS Apps 的曝光率
如何利用 Core Spotlight 框架增加 iOS Apps 的曝光率
In: Swift 程式語言

iOS 9 新增的 Search APIs 提供多個全新的APIs,而 Core Spotlight (CS) 是其中的一個框架,為開發者們顯著提升了App的可發現性,可見性和易用性,這是在歷代iOS版本中前所未見的。Search APIs 還讓使用者能夠更新更快地接觸開發者的App,加強了二者的連繫性。在iOS 9 中,除了 Core Spotlight 外,還有其他新的搜索特性,當中包括(這裡只作介紹):

  1. 新的方法和屬性存在與 NSUserActivity 類中(這是用來存儲App以便在稍後時間復原程式)。
  2. web markup 使網頁內容在裝置中被搜索到。
  3. universal links 讓App直接啟動網頁鏈接。

本文不會詳述以上三點,讓我們先聚焦在 Core Spotlight 框架的細節。開始之前,到底 Core Spotlight 是什麼呢。

過往 iOS 的搜索功能只可以讓使用者搜索到 Apple 原生的應用的資訊,例如在手機中的聯絡人。但透過新增的 Core Spotlight framework 使用者打破以往的被動性,不止官方的App,同時把開發者自訂的App都能搜索出來。這確是一個讓人振奮的變革。按說用戶可以與相關的自定義應用的搜索結果進行互動,不僅App將因為搜索結果記錄而自動啟動,開發者也同時將用戶吸引到特定視圖控制器(specific view controllers),為使用者從 Spotlight 得到更合適、更恰當的數據。

從開發者的角度來看,結合Core Spotlight框架並使用其提供的API並不牽涉複雜的程序。你即將會發現在本教學中,只需要幾行程式碼就可做到!其中的核心部份是讓開發者去「告知」iOS 關於應用數據的 索引(index)

正因本次教學是聚焦在 Core Spotlight 框架,周邊的細節就不作詳談。若果你也是樂於研究如果把所學實踐,請繼續本文以下的部份,在文章結束之前學會如果簡易地讓你的自訂App被 Spotlight 識別出來。

關於範例App

我們將一如既往的通過使用範例App來深入了解主題的細節。在這裡,我們先把一些數據集合到App之中,然後讓這些數據被裝置(或模擬器)中的 Spotlight 所搜尋到。首先,我們必須先來了解一下這個App的部份細節。

這次的範例是去展示一些電影資訊,比如內容摘要、導演、明星陣容、評級等等。所有這些資訊會被放置在一個 tableview 之中,當按下其中一列時,指定電影的細節資訊將會被展示在一個新的 view controller。通過這個程度,讓我們理解 Core Spotlight 的 API 是如何運作。這次範例所引用的資料數據乃擷取自 International Movie Database (IMDB)

從以下的動畫可以看到這個範例App的操作概念。

Core Spotlight Demo

這個教程有兩大目標:首要的是把所以相關的數據放入並讓 Spotlight 搜尋到,讓使用者在 Spotlight 中輸入關鍵字時,範例App內的數據能在查詢結果中顯示出來。而設定關鍵字是很重要的一環,稍後的時間我們將會處理這必要的程序。

第二個目標是通過按下搜索結果讓App被打開。假如我們沒有進行這個動作,默認的 view controller 會被加載並呈現出來,就這裡來說就是 tableview 內所蘊含的全部電影列表的 view controller。不過,這不是我們預想中的使用體驗。較理想的做法是,我們的範例App能夠直接在 Spotlight 的搜索結果中展示被選擇的電影細節。簡而言之,這不僅會讓我們的電影數據在 Spotlight 中被搜索出來,同時也會當 Spotlight 中的項目被點擊時,App會被打開並展示相關的細節。以下例子會讓這些更清晰:

Core Spotlight Final Demo

為了節省時間,你可以在這裡下載範例檔。內含:

  • 預先構建的UI,已包含所有必要的 IBOutlet 屬性。
  • 支援基本實作的 tableview。
  • 所有的電影數據存放在 .plist 文檔中。另外,還有一些圖片用來搭配對應的電影(一共5張)。

透過下列圖片,你可以了解列表裡的電影數據屬性和類別:

t46_3_movie_plist_sample

在看到 Core Spotlight API 更多的細節之前,我們將執行兩個任務:

  1. 載入和放置電影數據到 tableview 之中。
  2. 在 detail view controller 裡展示被選擇的電影資訊。

雖然這兩項可以預載在範例檔之中,但我有信心通過以下的教學,會讓你更能具體了解這些數據是如何被 Spotlight 搜索到出來。不用擔心,所需的前期工作不多,簡單幾步就能快捷完成。

加載和展示樣辦數據

當你已經下載好了範例檔並看到有關電影數據的plist檔,我們就開始吧。在 MoviesData.plist 檔案中包含了5個記錄,這些都是從IMDB網站上隨意地選取出來的。我們首要的目標是把數據從 .plist 檔案中加載到陣列裡,然後在 tableview 呈現出來。

直接進入程式碼,打開 ViewController.swift 檔,這是我們主要使用到的檔案,然後在最上方宣告以下屬性:

var moviesInfo: NSMutableArray!

所有的電影資料都會被加載到這個陣列之中,而每一部電影相應的關鍵字和數據都存放在字典 (dictionary) 之中。

讓我們先寫一個簡單的定義功能,使數據能夠進行加載。只要 .plist 檔案是確實存在,利用下列的程式編便能透過 NSMutableArray 自動讀取資料。

func loadMoviesInfo() {
    if let path = NSBundle.mainBundle().pathForResource("MoviesData", ofType: "plist") {
        moviesInfo = NSMutableArray(contentsOfFile: path)
    }
}

接著便要呼叫 viewDidLoad()。這必須要在 configureTableView() 之前設置:

override func viewDidLoad() {
    super.viewDidLoad()

    // Load the movies data from the file.
    loadMoviesInfo()

    configureTableView()
    navigationItem.title = "Movies"
}

雖說 loadMoviesInfo() 的程式碼可以直接寫入 viewDidLoad() 之中,但是以上的寫法可以讓程式碼更為簡潔。

當App啟動時都能夠加載電影資料時,接著便可以繼續並優化現行的 tableview 以展示更精確的資料。我們將會依據電影來決定列的數量,然後將屬性展示在 tableview 的 cell 上。

很明顯的,列的數量應該是和電影的數量相等的。若果遇上不存在檔案內的資料查詢,App就會出現「閃退」的情況。

func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    if moviesInfo != nil {
        return moviesInfo.count
    }

    return 0
}

最後就是把這些電影數據展示出來。在範例檔裡你可以找到一個叫 UITableViewCell 的子類別,並被命名為 MovieSummaryCell,對應 .xib 檔案,每個 cell 將顯示出相關的電影資料:

t46_4_custom_cell_ib

每一個 cell 都包含了相關電影的圖片、標題、部分的描述和評價。而所有的 UI 元件都連接著 IBOulet 屬性,並能夠在 MovieSummaryCell.swift 檔案中找到。

@IBOutlet weak var imgMovieImage: UIImageView!

@IBOutlet weak var lblTitle: UILabel!

@IBOutlet weak var lblDescription: UILabel!

@IBOutlet weak var lblRating: UILabel!

上述名稱都表示了每個屬性的目的,在接下來就讓他們與相關的電影資料做關聯。回到 ViewController.swift ,在 tableview 中更新以下的程式碼:

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 字典的使用並非是必須的,然而這讓整個程式碼看起來清晰易讀。

現在你可以首次執行一次App,你會看到細列出來的電影資料。直至目前為止都是耳熟能詳的東西,接著就去第二個預備步驟:將電影的細節部份展示出來。

展示數據細節

MovieDetailsViewController 類別中,我們將會利用 ViewController 把電影的細節從 tableview中提取出來。相應的場景都已經在 Interface Builder 中存在,我們只需要處理兩件事:從 ViewController 中傳遞合適的電影字典到這個類別之中,然後把字典裡的適用數值放到UI

展示每一個從屬於 ViewController 類的tableview中被選擇的電影的細節。各自的場景都已經在Interface Builder中存在了,所以我們將做兩件事情:從 ViewController 傳遞合適的電影字典到這個類中,然後放置那些字典裡面的值到合適的UI元件上,就是那些你能看到的所有被宣告和連接的 IBOutlet 屬性。

為了與字典進行對話,讓我們接下來在 MovieDetailsViewController 類別裡最上方做一個宣告:

var movieInfo: [String: String]!

回到 ViewController.swift 檔案,當電影列被觸及後,從 moviesInfo 陣列中挑選合適的字典傳遞至下一個 view controller 上,同時間 idSeguesShowMovieDetails 的 segue 會被執行。這個動作很簡單,但需要自訂屬性來儲存,所以在 ViewController 的最上方要做此宣告:

var selectedMovieIndex: Int!

然後,我們需要去處理tableview中的列被選擇時需要執行的方法:

func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
    selectedMovieIndex = indexPath.row
    performSegueWithIdentifier("idSegueShowMovieDetails", sender: self)
}

這部份處理兩項簡單的動作:第一是儲存被按下的列的索引到自訂的屬性內,第二是然行 segue 讓電影細節能展示出來。可是我們還沒有把合適的電影字典從 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]
        }
    }
}

很簡單,我們已經通過segue的屬性 destinationViewController 去取得到了 MovieDetailsViewController ,也讓合適的電影字典配對了剛剛宣告的 movieInfo 上。

現在,再一次打開 MovieDetailsViewController.swift檔案,宣告一個自定的方法。這部份只需要從 movieInfo 字典裡分配合適的值到相應的UI元件中,如下列這個簡單的實作:

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()
    }
}

這部份便完成好了。你可以先把App試著用,看看 tableview 中選擇的每一個電影資料。

對於Spotlight的索引數據

透過使用 iOS 9 的 Core Spotlight 框架,任何一個應用的數據都可以通過Spotlight被搜索到。關鍵是當使用者搜尋特定資料時,我們如何從 Core Spotlight 的 API 中適當地把資料提取出來。這不是我們這個範例App或 Core Spotlight API 去選擇什麼資料會被使用,我們應要去準備資料並為 API 提供特定的表格。

詳述一下,那些我們想要通過 Spotlight 被搜尋到的數據都必須在 CSSearchableItem 物件中被描述出來,然後將它們以陣列的方式組合並用CS API做出索引。 單一 CSSearchableItem 物件包含了一些屬性讓iOS以更簡潔的方式來找到每一個能被搜尋的項目,比如說什麼數據應該在搜索的時候被展示出來(例如電影的名字,它的圖片和描述),還有是什麼關鍵字導致我們應用中的數據被Spotlight搜尋到之類。所有這些屬性都包含在一個叫做 CSSearchableItemAttributeSet 的物件中,它可以提供許多屬性讓我們去分配我們需要的值。如果你需要,我給你官方文檔鏈結,這裡你就可以看到所有支援的屬性。

最後的動作是為 Spotlight 中的數據設置索引。通常來說按照以下流程:

  1. 為每一段數據設置它們獨有的屬性,比如一個電影( CSSearchableItemAttributeSet )。
  2. 從之前的程序利用屬性把每一個被搜索的項目初始化( CSSearchableItem )。
  3. 把這些可被搜索的項目都放入一個陣列中。
  4. 為陣列中的每一個項目設置一個索引。

我們將一步一步來。我們將在 ViewController.swift 檔中創立一個叫做 setupSearchableContent() 的自定義方法。在這部份最後的實作中,讓你的數據變得可搜索化並不難。在實作之前,我們先把程式碼拆散,讓你可以簡單地去理解它。

在此之前,我們先 import 兩個框架:

import CoreSpotlight
import MobileCoreServices

然後我們便可以為新方法下定義,同時間把收集可搜索的項目集合的陣列作出宣告:

func setupSearchableContent() {
    var searchableItems = [CSSearchableItem]()
}

在迴圈中我們開始分配每一個電影:

func setupSearchableContent() {
    var searchableItems = [CSSearchableItem]()

    for i in 0...(moviesInfo.count - 1) {
        let movie = moviesInfo[i] as! [String: String]
    }
}

對每個電影,我們都會創建一個 CSSearchableItemAttributeSet 物件,然後給那些給 Spotlight 搜索的數據設置屬性。在我們的範例中,我們會明確地指出電影的標題、描述和圖片來作為我們想給使用者看到的數據片段。

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"]!
    }
}

注意上面代碼片段中我們是怎麼將電影圖片設置為屬性的。其實有兩種方法來實現:無論是使用圖片的URL地址,或者使圖片成為 NSData 物件。最簡單的方式就是為每一個電影圖片提供URL地址,因為我們知道這些圖片都存在在應用包(application bundle)中。然而,這個方式需要打破每個圖像文件的實際名和延伸,所以我們將使用String類中的 componentsSeparatedByString: 方法來分開他們。剩下的都不難去理解。

現在我們該為那些想在Spotlight中被找到的應用數據設置關鍵字了。在想一些指定關鍵字之前,你必須明白你的決定絕對會影響是你的 App 在 Spotlight 被用戶搜索的機率。在範例中我們將設置關鍵字包括電影的類型和它的評級。

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 陣列中以方便存取。然後將每一個值分別以迴圈加入 keywords 陣列中。以同樣的步驟設定電影評級,然後把它們加入 keywords 陣列中。

很重要的最後一句。我們為每一個電影的關鍵字加入相關屬性。把這行忘記了意味著在Spotl中對我們的應用搜索不到任何結果。若果把這一點遺漏,就會使 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)
    }
}

以上初始化接受了三個參數:

  • 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,利用在 Spotlight 搜索關鍵字並離開它。這個時候搜索結果會呈現你的眼前。隨卜廿心日點下任何一個搜索結果,App便會被自動啟動。

Core Spotlight Demo 2

實現指向性登入

通過 Spotlight 中搜索我們的App中的電影數據是令人振奮的,但我們可以做得再好一點。至目前為止,在 Spotlight 觸發項目讓這App進行啟動並且在 ViewController 展示出來,但我們的目標是讓被選擇的電影項目指向detail view controller中,並且看到更多的細節。

即使看起來很困難或複雜,其實實際運用起上來卻很簡單。在這範例中展示出來的項目相對容易,因為我們是基於現有的東西,便於管理及展示選定電影的細節。

主要的工作是覆寫 UIKit 方法並命名為 restoreUserActivityState:,然後在 Spotlight 中處理被選擇的項目。我們最終想達到的,是從可搜索化項目中用識別標誌取出moviesInfo 陣列中電影的索引(如果你還記得,我們在先前的部份中創建了動態性的識別標誌),然後我們使用它傳遞適合的電影字典並展示到 MovieDetailsViewController 視圖控制器中。

上述方法的參數是一個 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 的類別。說真的,在這個範例App中這項檢查並不重要,但若果你的App要處理多個 NSUserActivity 物件,這步驟就是不可或缺(比如說,在iOS 8 中使用 NSUserActivity 分類的 Handoff 特性)。識別標誌在 userInfo 字典中是一個字串值,當我們得到這個值,由於我們使用點符號去分隔字串值組合的元件,然後我們便可以利用 componentsSeparatedByString(“.”) 去分隔不同元件,並取得最後一個物件作為被選擇的電影索引。剩下來就簡單多了:我們分配出 selectedMovieIndex 屬性的索引,然後執行 segue。我們先前實作的東西會解決剩下的所有問題。

現在轉到 AppDelegate.swift 檔案中。我們必須在其中去實現一個目前還沒存在的代理方法(delegate function)。當關聯到我們App的項目被觸發時,該方法每次都會被呼叫到,我們現需要就是實現這個被呼叫的方法,而這東西當然是通過 user activity 來實現的。實作如下:

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 分類並發出一個自定通知,但上述的方法比較直接了當。

全部搞定!這個範例App已經完成,再運行一次並看看當你觸發 Spotlight 項目會發生什麼。

Core Spotlight Final Demo

總結

對開發者來說,在iOS 9中新的搜索API是非常有被看好的,因為這允許Apps能夠更容易被用戶發現和訪問。在這個教程中,我們通過這些方式去建立App數據的索引,並讓它們更容易在 Spotlight 被發現,以及一個被選擇的項目是如何通過處理App中的特殊數據以展示給使用者看。在你的App中實作這些與衆不同的特性是會提高用戶的體驗,所以,這是你應該慎重考慮把這個重要的API加進你當前和未來專案中。來到最後,我由衷希望這個教程能夠給予你有用的協助。祝愉快!

如果你需要的話,你可以到Github下載整個完整的Xcode專案.

對本文有任何疑問,請留言。衷心希望你能對本文發表任何評論。

譯者簡介:謝岳庭,中文系出身,自學iOS開發,有著異於常人的求知慾。你可以在TwitterAppCoda討論區中聯繫我並隨時歡迎你與我交流。

原文How to Use Core Spotlight Framework in iOS 9

作者
Gabriel Theodoropoulos
資深軟體開發員,從事相關工作超過二十年,專門在不同的平台和各種程式語言去解決軟體開發問題。自2010年中,Gabriel專注在iOS程式的開發,利用教程與世界上每個角落的人分享知識。可以在Google+或推特關注 Gabriel。
評論
很好! 你已成功註冊。
歡迎回來! 你已成功登入。
你已成功訂閱 AppCoda 中文版 電子報。
你的連結已失效。
成功! 請檢查你的電子郵件以獲取用於登入的連結。
好! 你的付費資料已更新。
你的付費方式並未更新。