使用 YouTube API 打造影音搜尋 App

大家都知道 Google 提供了許多數位產品和服務,用來滿足各式各樣的使用者需求。不過除了使用 Google 本身所提供的服務之外,人們(開發者)也會想要透過別種方式來存取這些服務。 Google 確實針對開發者提供了許多的協助,包括允許他們建立各種跨平台的應用程式,因為 Google 提供了許多不同使用方式的應用程式協定介面( Application Protocol Interface, API ),同時也針對不同的程式設計語言準備了所需的程式庫和 SDK 。


因為 Google API 和服務也能夠運用於行動平台,所以我們當然不能放過這個好好研究一番的機會,而且事實上,我們在過去也曾處理過 Google 技術。舉例而言,這篇文章講的是 Google Maps SDK 。而在本文中,我們將首度揭露 YouTube API 這項全然不同的服務。

YouTube API 非常容易使用,不過有些事情你最好先知道一下,否則使用起來可能會感覺卡卡的。請容我給你一點提示:我們將不會使用任何 SDK 或者 iOS 本身的程式庫。相反地,我們只會送出簡單的 HTTP 要求(其實就是 GET 要求),向 Google 擷取資料。結果會是 JSON 格式,所以你最好先知道一下 JSON 格式的資料如何編碼。你不必是 JSON 專家;只需要了解其格式即可。如果需要協助的話,可以在這裡找到一些有用的資訊。

Google 關於 YouTube API 的文件數量龐大,所以本文的目標是將收集自不同網頁的資訊集合在一起,並協助你快速且容易地使用這些 API 。雖然我們只會介紹一小部分的 YouTube API ,但是你學到的知識將可以幫助你理解其餘的部分。不過在繼續往下閱讀之前,我必須先提醒你幾件事。

如前所述,我們會向 YouTube API 送出 HTTP 要求以便取得資料。這些要求會觸發執行特定的函式,實際上代表的是特定的 API 任務。舉例而言, channels.list 函式可以取得頻道( Channel )的資料。當然了,針對本文所要探討的每個函式( HTTP 要求),我都會提供相關文件的連結。每個函式都需要一些參數和篩選器。在介紹這類函式用法的小節當中,我會說明即將使用哪些參數和篩選器,至於細節的部分,可以點擊文章中的連結以便閱讀更多的資訊。在此就不贅述 Google 文件的內容了。

針對向 Google API 發出的要求,其中有些會需要使用者授權,有些則不會。針對需要授權的那些 API ,使用的是 OAuth 2.0 協定,好讓使用者能夠安全地以 Google 帳號登入。至於不需要授權的那些 API ,只需要簡單的 API 金鑰就可以用來應付不需要授權的要求了。在本文中,我們只會處理第 2 種情況,也就是說我們並不會要求使用者登入。但是請相信我,這些議題要用單篇文章講完並不容易。

我強烈建議你可以在閱讀本文的同時,搭配一些特定的 Google 文件。如果你想要先嘗鮮一下,可以參考下列這些連結:

除了上述這些連結之外,也歡迎隨意瀏覽並且閱讀任何你感興趣的內容。先說明一下,我們即將使用的是 YouTube API v3 ,這是 Google 目前建議採用的版本。最後容我再提醒一點,我們實在無法提供所有的連結(不只上述這些連結,還有接下來將會看到的那些連結),因為 Google 文件太過廣泛了。因此我有必要將本文的內容限縮在合理的範圍之內。

好的,就讓我們開始動手吧,在本文的最後,我們將會完成一支可以用來下載頻道和影片( Video )、搜尋以及播放影片的 App 。且聽我們娓娓道來。

範例 App 簡介

我將從下載 Starter 專案開始介紹我們要在本文中開發的範例 App 。在此壓縮檔中,你可以看到介面和所需的 IBOutlet 屬性以及 IBAction 函式,還有其他必要的程式碼,所以我們只需要專注於實作 App 的邏輯就可以了。

注意:你需要使用 Xcode 7 以上版本來執行此範例程式。

在開始撰寫程式碼之前,我們必須從 Google 取得 API 金鑰,好讓我們能夠向 YouTube API 發出要求。我們需要遵循一道特定的程序,不過我現在還不打算詳細介紹;在下一節中我們才會一步一步說明。

現在讓我們先來看一下範例 App 的模樣:

youtube-api-demo

此範例 App 具備多重目標:

  • 下載(並顯示) YouTube 頻道的詳情。
  • 下載(並顯示)頻道中的影片播放清單。
  • 能夠搜尋頻道和影片。
  • 播放影片。

關於使用者介面的部分,此 App 是由 2 個視圖控制器所組成。第 1 個視圖控制器擁有 3 個子視圖:表格視圖、文字欄位、分段元件。這些子視圖的用途分述如下:

  1. 在表格視圖中,將會顯示頻道或影片的資訊。這些影片可以是頻道的播放清單,或是搜尋的結果。
  2. 文字欄位是搜尋的欄位。在此輸入的關鍵字隨後會向 YouTube 發出搜尋要求。
  3. 分段元件非常重要。當選取的是第 1 個索引時,表格視圖將會顯示頻道。此外搜尋結果也只會侷限於頻道。另一方面,當選取的是第 2 個索引(影片索引)時,表格視圖將會顯示現有的影片播放清單,而搜尋結果也只會與影片有關。

第 2 個視圖控制器是我們的播放器視圖控制器;用來播放選取的影片的串流。我們會在最後一個小節介紹這個部分。此外我們將會使用 Google 的輔助程式庫來「建立」播放器並且實現串流。

我們對於範例 App 的規劃如上所述,現在就讓我們開始來實作吧。

建立 API 金鑰

探索 YouTube API 的第一步,就從這裡開始:在 Google Developers Console 中替我們的 App 建立一筆新的記錄(新的專案),以便能夠存取 YouTube API 。事實上,在 Console 中建立完新的專案之後,還需要做 2 件事:

  1. 啟用( Enable ) YouTube API ,以便在我們的 App 中使用。
  2. 建立 API 金鑰,以便向 YouTube API 發出要求。

請留意(再度提醒),我們在本文中向 YouTube API 發出的要求並未經過授權程序,這意味著我們不會要求使用者以他們的 Google 帳號來登入我們的 App 。當然了,只是透過運用 API 金鑰,的確也限制了能夠發出的要求,如果想要完整的存取權限,則需要完整的授權。不過就本文的內容而言,我們只需要使用 API 金鑰就夠了。

現在請連結到 Developers Console 並且為我們的專案建立新的記錄。假使你目前並未登入 Google 的話,請即刻登入。必須要有 Google 帳號才能夠繼續遵循本文的步驟。登入之後看到的會是如下所示的畫面:

t39_2_console_projects

點擊左上角的藍色 Create Project (建立專案)按鈕,在彈出視窗中輸入新專案的名稱。我命名為 YTDemo ,不過你可以選擇任何你喜歡的名稱。請留意,如果你覺得已經不再需要此專案的話,最後也可以從 Console 刪除這筆記錄。

t39_3_console_project_name

下一步是啟用此專案的 YouTube API 功能。記住,你可以為同一個專案啟用多組 API ,不過此刻你還不需要使用到其他的 API 。

啟用 YouTube API 的方法是,從視窗左側的功能表中點擊 APIs & auth > APIs 選項。在主視窗區域中找到 YouTube Data API ( YouTube 資料 API )並且點擊其連結。

t39_4_console_youtube_api

瀏覽器會將你帶領到新視窗,你可以(也必須)在那裡啟用 YouTube API 。只要點擊上方的 Enable API 按鈕,一切就準備就緒了。

現在我們已經啟用完 YouTube API 了,接著必須建立我們想要使用的 API 金鑰。來到 APIs & auth > Credentials 功能表,點擊 Create new Key (建立新的金鑰)按鈕之後將會出現新視窗。在彈出視窗中,必須選取我們的專案所適用的平台。這時候我打賭你會毫不猶豫地點擊 iOS key ( iOS 金鑰)按鈕。但是請別這麼做。根據 Google 的文件, iOS 金鑰無法適用於所有的 API ,而 YouTube API 正好就是其中一組。我可以證明這點,因為基於好奇我曾嘗試著建立這樣一把金鑰,並且套用於範例 App 中,但是完全無法使用。

t39_5_console_key_options

你應該做的,其實是點擊 Browser key (瀏覽器金鑰)按鈕。在彈出視窗中,只要點擊 Create 即可,不要在欄位中輸入任何文字。接著回到帳號密碼的憑證視窗,你將會發現新的金鑰已經建立完成。如你所見,你可以套用各種動作,甚至可以刪除,但是我們現在當然還不會這麼做。花了一些時間,我們在 Developers Console 中的設定工作總算大功告成,但是還不要登出。讓瀏覽器繼續開著,因為你很快就會需要將產生的金鑰複製到 iOS App 當中。

擷取資料的機制

在產生完 API 金鑰之後,我們便可以返回 Xcode 開始處理 Starter 專案。如果仔細回想我們想要實作的目標和功能(取得頻道和影片、實現搜尋),將會意識到存在著一個共同的元素。亦即我們都必須向 Google 的伺服器發出要求( HTTP 要求),接著傳回 JSON 格式的資料。牢記這項事實,如果我們可以撰寫通用的函式來實現實際的要求以及取回資料,一定會相當方便。

沒錯,你猜對了,本節我們要做的正是這件事。更精確地說,在我們的 ViewController 類別中將會實作一個可以用來進行簡單的 GET 要求的函式。在我們的範例 App 中,我們使用的是 NSURLSessionNSURLSessionDataTask 類別。請留意,這些動作都是非同步的,因此只要一收到任何結果(或錯誤訊息),我們就必須使用主執行緒來加以剖析然後更新 UI 。相關步驟詳述如下。

首先,定義此函式:

func performGetRequest(targetURL: NSURL!, completion: (data: NSData?, HTTPStatusCode: Int, error: NSError?) -> Void) {

}

如你所見,此函式預期我們將會提供要求的 URL 網址( NSURL 物件)。第 2 個參數是閉包( Closure ),或稱完成處理常式區塊( Completion Handler Block )。我在前面提過,實際的資料擷取動作是非同步的,當資料擷取的動作做完時,此完成處理常式便會在主執行緒中被喚起,以便執行剖析的任務。請留意,此完成處理常式需要 3 個參數:

  1. data:擷取到的資料,格式為 JSON 。
  2. HTTPStatusCode: HTTP 狀態代碼。我們希望收到的代碼是 200 (代表一切正常)。
  3. error:發生錯誤時,則會傳回錯誤訊息。

現在,讓我們進入到實作的部分:

func performGetRequest(targetURL: NSURL!, completion: (data: NSData?, HTTPStatusCode: Int, error: NSError?) -> Void) {
    let request = NSMutableURLRequest(URL: targetURL)
    request.HTTPMethod = "GET"
    
    let sessionConfiguration = NSURLSessionConfiguration.defaultSessionConfiguration()
    
    let session = NSURLSession(configuration: sessionConfiguration)
    
    let task = session.dataTaskWithRequest(request, completionHandler: { (data: NSData?, response: NSURLResponse?, error: NSError?) -> Void in
        dispatch_async(dispatch_get_main_queue(), { () -> Void in
            completion(data: data, HTTPStatusCode: (response as! NSHTTPURLResponse).statusCode, error: error)
        })
    })
    
    task.resume()
}

讓我們來探討一下上述的程式碼。首先,我們利用參數裡的 URL 物件來建立 NSMutableURLRequest 物件。我們必須設定 GET 作為偏好的 HTTP 方法。這樣就組成了稍後要送出的要求。接著,初始化 NSURLSessionConfiguration 物件,並將之傳入作為初始化 NSURLSession 的參數。然後利用此 session (工作階段)實體來建立資料任務物件,此時我們提供 request (要求)作為參數。在資料任務的完成處理常式中,我們呼叫了 dispatch_async() 函式,好讓我們能夠在主執行緒中執行我們的完成處理常式。請留意我們如何從這些參數當中取得回應( NSURLResponse )的 HTTP 狀態代碼。最後,我們使用資料任務的 resume() 函式來開始擷取作業。

現在,就只差資料的剖析了,不過這件事我們稍後才會做。現在可以確定的是我們不需要擔心資料的擷取了,因為每當我們需要的時候,都可以利用上述函式來達成任務。

取得頻道資訊

我們「要求」 YouTube API 做的第一件事,就是提供一份頻道清單。說得更清楚些,我們會要求標題( Title )、簡介( Description ),以及 Apple 、 Google 和 Microsoft 的 YouTube 頻道的縮圖( Thumbnail )資料。除了這些資料之外,我們還需要一個額外的數值,也就是每個頻道上傳的影片的播放清單 ID ,好讓我們在稍後能夠向這些影片發出要求。

所有這些任務都會寫在同一個新函式裡。在此函式中,我們不只會取得單一頻道的詳情,我們的實作會按照此邏輯以遞迴方式取得所有頻道的詳情。在開始撰寫函式之前,讓我們先來宣告一些將會使用到的變數。請來到 ViewController 類別的開頭,並且加入下列這幾行:

var apiKey = "YOUR_API_KEY"

var desiredChannelsArray = ["Apple", "Google", "Microsoft"]

var channelIndex = 0

var channelsDataArray: Array> = []

請確定在 apiKey 變數中設定了你自己的 API 金鑰數值。在複製和貼上的時候請務必留意,因為如果有誤的話,稍後送出的要求將會失敗。

除了 API 金鑰變數之外,我們還需要另外 3 個變數:

  • desiredChannelsArray:此陣列包含了我們想要取得其資訊的頻道的名稱。
  • channelIndex:此變數用來指向 App 應該在稍後即將實作的函式中向哪個頻道發出要求。
  • channelsDataArray:此陣列將會擁有取得的頻道的資料。更精確地說,就是我們從 JSON 回應中擷取出來的資料,每個元素都是一筆頻道字典記錄。

關於我們要使用的 YouTube API 函式,說明如下。此函式是 channels.list ,相關資訊可以在 這裡找到。此要求的 URL 網址如下:

https://www.googleapis.com/youtube/v3/channels

此 URL 還需要一些參數,以便指定或篩選要傳回的資料。在介紹這些參數之前,容我先說明一下,針對頻道詳情的取得,總共有 3 種呼叫 API 的方式:

  1. 指出你是頻道的擁有者。
  2. 指定擁有者的使用者名稱。
  3. 提供頻道 ID (每個頻道有各自的唯一數值)。

在上述這 3 種方法當中,我們將會使用到後面 2 種。針對這 2 種方法,我們都會使用 part 作為必要參數,用來指定要求的結果要包含哪些部分。在本例中,我們針對此參數的設定是 snippet, contentDetails (以逗號分隔數值),其中 snippet 可以為我們提供頻道的詳情(我們會在稍後加以顯示),而 contentDetails 則會傳回該頻道所擁有的上傳影片的播放清單 ID 。在基於使用者名稱來擷取頻道的情況當中,我們還會使用到 forUsername 參數,並且使用 id 參數來取得該 ID 所代表的頻道的資訊。

此刻請容我提醒一點,雖然就目前而言我們並不需要使用該 ID 來取得頻道詳情,不過稍後在搜尋頻道時還是會需要用到。此刻我只是先將這項資訊讓你知道而已。

除了要指定的參數之外,我們每次都需要使用 API 金鑰,否則要求將會失敗。

請牢記上述的說明,現在就讓我們開始來實作新函式吧。你將會看到,參數的數值指出了發出要求的偏好作法:

func getChannelDetails(useChannelIDParam: Bool) {
    var urlString: String!
    if !useChannelIDParam {
        urlString = "https://www.googleapis.com/youtube/v3/channels?part=contentDetails,snippet&forUsername=\(desiredChannelsArray[channelIndex])&key=\(apiKey)"
    }
    else {

    }

    let targetURL = NSURL(string: urlString)
}

請留意,上述程式碼中的 else 區塊是空白的。我們稍後才會補上,此處的示範可以看到根據頻道 ID 數值所組合出來的 URL 網址:

urlString = "https://www.googleapis.com/youtube/v3/channels?part=contentDetails,snippet&id=SOME_ID_VALUE&key=\(apiKey)"

另外也請留意,在組合完最終的 URL 字串之後,必須將之轉換成 NSURL 物件。此物件會緊接著在稍早實作過的 performGetRequest(...) 函式之後被呼叫到。

在繼續往下進行之前,請容我先為你展示使用 username 參數從 Apple 頻道傳回的 JSON 資料(若要實際測試此 API 呼叫,可以按這裡,你也可以試試看其他的要求類型):

{
 "kind": "youtube#channelListResponse",
 "etag": "\"Y3xTLFF3RLtHXX85JBgzzgp2Enw/vTcBxrLzIZMaCeqgOzHFbBhLTUs\"",
 "pageInfo": {
  "totalResults": 1,
  "resultsPerPage": 5
 },
 "items": [
  {

   "kind": "youtube#channel",
   "etag": "\"Y3xTLFF3RLtHXX85JBgzzgp2Enw/Wd_D4NnOqPTiOtqOKvlNDEMuMxY\"",
   "id": "UCE_M8A5yxnLfW0KghEeajjw",
   "snippet": {
    "title": "Apple",
    "description": "Apple designs the Mac, along with OS X, iLife, and iWork. It leads the digital music revolution with iPods and iTunes. It reinvented the mobile phone with iPhone and App Store. And it's defining the future of mobile media and computing with iPad.",
    "publishedAt": "2005-06-22T05:12:23.000Z",
    "thumbnails": {
     "default": {
      "url": "https://yt3.ggpht.com/-KdgJnz1HIdQ/AAAAAAAAAAI/AAAAAAAAAAA/4vVN7slJqj4/s88-c-k-no/photo.jpg"
     },
     "medium": {
      "url": "https://yt3.ggpht.com/-KdgJnz1HIdQ/AAAAAAAAAAI/AAAAAAAAAAA/4vVN7slJqj4/s240-c-k-no/photo.jpg"
     },
     "high": {
      "url": "https://yt3.ggpht.com/-KdgJnz1HIdQ/AAAAAAAAAAI/AAAAAAAAAAA/4vVN7slJqj4/s240-c-k-no/photo.jpg"
     }
    },
    "localized": {
     "title": "Apple",
     "description": "Apple designs the Mac, along with OS X, iLife, and iWork. It leads the digital music revolution with iPods and iTunes. It reinvented the mobile phone with iPhone and App Store. And it's defining the future of mobile media and computing with iPad."
    }
   },
   "contentDetails": {
    "relatedPlaylists": {
     "uploads": "UUE_M8A5yxnLfW0KghEeajjw"
    },
    "googlePlusUserId": "115967756576401290385"
   }
  }
 ]
}

針對上述的結果,我們關心的只有 titledescriptionsnippet 字典中的 default thumbnail ,而針對 uploads 數值,我們感興趣的只有 contentDetails > relatedPlaylists 字典。

根據上述的結果,我們可以「解碼」出每個我們想要的資料的路徑,以便完成後續的實作。為了簡化起見,我針對上述的每一行結果都在程式碼中加入了註解,方便你理解每個步驟的用途:

func getChannelDetails(useChannelIDParam: Bool) {
    ...

    performGetRequest(targetURL, completion: { (data, HTTPStatusCode, error) -> Void in
        if HTTPStatusCode == 200 && error == nil {
            
            do {
                // 將 JSON 資料轉換成字典
                let resultsDict = try NSJSONSerialization.JSONObjectWithData(data!, options: []) as! Dictionary
                
                // 從傳回的資料中取得第一筆字典記錄(通常也只會有一筆記錄)
                let items: AnyObject! = resultsDict["items"] as AnyObject!
                let firstItemDict = (items as! Array)[0] as! Dictionary
                
                // 取得包含所需資料的 snippet 字典
                let snippetDict = firstItemDict["snippet"] as! Dictionary
                
                // 建立新的字典,只儲存我們想要知道的數值
                var desiredValuesDict: Dictionary = Dictionary()
                desiredValuesDict["title"] = snippetDict["title"]
                desiredValuesDict["description"] = snippetDict["description"]
                desiredValuesDict["thumbnail"] = ((snippetDict["thumbnails"] as! Dictionary)["default"] as! Dictionary)["url"]
                
                // 儲存頻道的上傳影片的播放清單 ID
                desiredValuesDict["playlistID"] = ((firstItemDict["contentDetails"] as! Dictionary)["relatedPlaylists"] as! Dictionary)["uploads"]
                
                
                // 將 desiredValuesDict 字典新增到陣列中
                self.channelsDataArray.append(desiredValuesDict)
                
            } catch {
                print(error)
            }
            
        } 
    })
}

讓我們來看幾個重點。首先,我們檢查 HTTP 狀態代碼的數值和錯誤物件,以便確認真的有 JSON 資料存在。如果一切正常的話,我們會開始將擷取到的資料( NSData 物件)轉換成字典。接著,我們使用此字典來存取傳回來的所有項目(與頻道資料有關的字典)。通常在要求頻道的詳情資料時,只會傳回一個項目。在本例中,此項目是以 firstItemDict 表示的物件。這樣就可以很方便地取得 snippet 字典。

在存取完 snippet 字典之後,為了保存目的,我們建立了一個新的字典,因為我們需要保留那些數值。在 desiredValuesDict 字典中,我們保存了頻道的標題、簡介和縮圖網址。請留意,我選擇使用預設的縮圖(尺寸最小的那個)。必須稍微「挖掘」一下,才能夠找到此數值。

在處理完頻道的詳情(包括標題等)之後,我們存取 uploads 數值以便取得包含所有頻道影片的播放清單 ID 。如你所見,此數值也是存放在 desiredValuesDict 字典當中。

最後,將我們自訂的字典附加到 channelsDataArray 陣列中,以便稍後能夠顯示和管理這些頻道。

上述的函式尚未實作完成,不過在繼續往下討論之前,容我先介紹即將用來擷取所需數值的結構順序(我們稱之為「路徑」):

要存取 snippet 字典,使用:
從 JSON 轉換而來的字典 > “items” 陣列 > 第一個項目字典 > “snippet” 字典

要存取頻道的縮圖網址,使用:
從 JSON 轉換而來的字典 > “items” 陣列 > 第一個項目字典 > “snippet” 字典 > “thumbnails” 字典 > “default” 字典 > “url” 數值

要存取播放清單 ID ,使用:
從 JSON 轉換而來的字典 > “items” 陣列 > 第一個項目字典 > “contentDetails” 字典 > “relatedPlaylists” 字典 > “uploads” 數值

如你所見,必須深入到 JSON 結果底層的字典和陣列當中,不過這一點也不困難,而且 Google 提供的範例結果也很清楚易懂,對於學習非常有幫助。

現在讓我們開始來實作缺漏的部分吧。接下來要做的,是在頻道資料新增到 channelsDataArray 陣列之後,重新載入表格視圖(雖然我們要到下一節才會實作顯示資料的功能),並且繼續擷取下一個頻道(當然是指如果還有的話)。

func getChannelDetails(useChannelIDParam: Bool) {
    ...

    performGetRequest(targetURL, completion: { (data, HTTPStatusCode, error) -> Void in
        if HTTPStatusCode == 200 && error == nil {
            ...

                // 重新載入表格視圖
                self.tblVideos.reloadData()
                
                // 載入下一個頻道資料(如果有的話)
                ++self.channelIndex
                if self.channelIndex < self.desiredChannelsArray.count {
                    self.getChannelDetails(useChannelIDParam)
                }
                else {
                    self.viewWait.hidden = true
                }
            } catch {
                print(error)
            }
            
        } 
    })
}

self.viewWait.hidden = true 可以確保在擷取完所有的頻道之後,活動指示器和透明視圖會從螢幕上消失。

最後,我們要處理的是 HTTP 狀態代碼不是 200 或是遭遇錯誤的情況。無論是哪一種情況,我們都只會在主控台上顯示一段訊息,不會再做進一步的處理:

func getChannelDetails(useChannelIDParam: Bool) {
    ...

    performGetRequest(targetURL, completion: { (data, HTTPStatusCode, error) -> Void in
        if HTTPStatusCode == 200 && error == nil {
            ...
        } else {
            print("HTTP Status Code = \(HTTPStatusCode)")
            print("Error while loading channel details: \(error)")
        }

    })
}

我們的新函式已經準備就緒了。只需要加以呼叫即可,我們打算在 viewDidLoad 函式中這麼做:

override func viewDidLoad() {
    ...

    getChannelDetails(false)
}

透過檢視 getChannelDetails(...) 函式,最終你將發現,我們做的事情其實一點也不複雜。我們只是處理傳回的資料,並且將我們感興趣的部分儲存起來。在接下來的小節中,我們還會實作幾個也很類似的函式;當我們要取得頻道的影片,以及當我們在實作搜尋功能的時候。不過那一點也不麻煩,因為你已經受過剛剛那陣呼叫 YouTube API 的「洗禮」了。

顯示頻道

如同前面說明過的,在 ViewController 類別中的分段元件扮演了很重要的角色,實際上我們就是透過它來決定要在表格視圖中顯示哪種資料。當選取的是名為 Channels (頻道)的第 1 個索引時,顯示的應該會是對應的頻道詳情,相反地,如果選取的是影片索引,則會在表格視圖中顯示影片的詳情。此刻就應該考慮這個部分,因為我們現在就必須依照條件在表格視圖中顯示擷取的頻道資料。

好了,首先我們要指定的是表格視圖的資料列數目:

func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    if segDisplayedContent.selectedSegmentIndex == 0 {
        return channelsDataArray.count
    }
    else {

    }

    return 0
}

如你所見,要決定搭配的資料來源非常簡單,只需要根據 segDisplayedContent 分段元件的 selectedSegmentIndex 屬性。我們便可以據此在表格視圖中顯示對應的儲存格資料:

func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    var cell: UITableViewCell!

    if segDisplayedContent.selectedSegmentIndex == 0 {
        cell = tableView.dequeueReusableCellWithIdentifier("idCellChannel", forIndexPath: indexPath)   
    }
    else {
        cell = tableView.dequeueReusableCellWithIdentifier("idCellVideo", forIndexPath: indexPath)
    }    

    return cell
}

如果檢視 Interface Builder 中的 View Controller 場景,將會注意到頻道的原型儲存格包含了 3 個子視圖,並且分別被指派了特定的 tag (標記)數值:

  1. 頻道標題是 UILabel ,其 tag 數值為 10
  2. 頻道簡介是 UILabel ,其 tag 數值為 11
  3. 頻道縮圖是 UIImageView ,其 tag 數值為 12

t39_6_channel_prototype_cell

我們首先要做的是從每個儲存格中將這些子視圖「抽取」出來,以便將擷取到的資料填進去。示範如下:

func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    var cell: UITableViewCell!

    if segDisplayedContent.selectedSegmentIndex == 0 {
        cell = tableView.dequeueReusableCellWithIdentifier("idCellChannel", forIndexPath: indexPath)

        let channelTitleLabel = cell.viewWithTag(10) as! UILabel
        let channelDescriptionLabel = cell.viewWithTag(11) as! UILabel
        let thumbnailImageView = cell.viewWithTag(12) as! UIImageView

    }
    else {
        cell = tableView.dequeueReusableCellWithIdentifier("idCellVideo", forIndexPath: indexPath)
    }

    return cell
}

接著,我們取得每個頻道的詳情,並且指派給這些子視圖:

func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    var cell: UITableViewCell!

    if segDisplayedContent.selectedSegmentIndex == 0 {
        ...

        let channelDetails = channelsDataArray[indexPath.row]
        channelTitleLabel.text = channelDetails["title"] as? String
        channelDescriptionLabel.text = channelDetails["description"] as? String
        thumbnailImageView.image = UIImage(data: NSData(contentsOfURL: NSURL(string: (channelDetails["thumbnail"] as? String)!)!)!)
    }
    else {
        cell = tableView.dequeueReusableCellWithIdentifier("idCellVideo", forIndexPath: indexPath)
    }

    return cell
}

現在你可以嘗試首次執行此 App 。如果你有遵循剛才所介紹的每個步驟,也設定了自己的 API 金鑰,那麼應該可以在表格視圖中看到 3 個頻道。

t39_1_channel_sample

擷取影片

範例 App 的第一項功能現在運作正常,接下來繼續實作第二項。這次我們想要擷取所選頻道的影片,並且條列在表格視圖中(我們將在下一個小節中實作此部分)。整個程序顯然會在頻道被點擊時才展開,這也正是我們實作的起點。

一如往常,我們需要在類別的開頭宣告新的變數:

var videosArray: Array> = []

如同你所猜測的,我們會在剖析之後,在 videosArray 陣列中存放影片的詳情。請留意,陣列在宣告的同時也一併進行了初始化。

現在,讓我們來處理頻道儲存格的點擊事件:

func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
    if segDisplayedContent.selectedSegmentIndex == 0 {
        // 在本例中,頻道就是要顯示的內容
        // 所選頻道的影片將被擷取並顯示出來

        // 將分段元件切換成「 Videos 」
        segDisplayedContent.selectedSegmentIndex = 1

        // 顯示活動指示器
        viewWait.hidden = false

        // 移除 videosArray 陣列中全部舊有的影片詳情
        videosArray.removeAll(keepCapacity: false)

        // 針對點擊的頻道,擷取其影片詳情
        getVideosForChannelAtIndex(indexPath.row)
    }
    else {

    }
}

上述的註解可以幫助你理解整個流程。因為我們要顯示的是影片,所以首先將選取的分段元件索引變更為適當的數值。接著,我們顯示包含活動指示器的 viewWait 視圖。不容忽視的一個重要步驟就是要將 videosArray 陣列中的舊內容清除乾淨。當然了,此 App 首次執行時此陣列一定是空白的,但是後續的影片要求就不能夠缺少這行程式碼了。最後,我們呼叫了 getVideosForChannelAtIndex(...) 函式,也就是我們在此想要實現的功能。但是此函式尚不存在,所以我們稍後將會實作。請留意,我們會將選取的儲存格的索引以參數形式傳遞進去。

這回我們將會使用到 YouTube API 的 playlistItem.list 函式,相關的文件可以參考這裡。我們會在此 GET 要求中輸入 3 個參數;其中一個顯然就是 API 金鑰。除此之外,我們還需要設定必要的 part 參數搭配 snippet 數值,以便取得所需的影片詳情,以及包含這些影片的播放清單的 ID 。此參數叫做 playlistID 。針對所擷取的頻道,我們已經取得其播放清單的 ID 數值,接著便可以加以使用。假使你想要觀看實際使用該要求的範例,可以造訪這個網頁(確定 part 參數只有設定 snippet 數值)。

我們將開始實作此新函式,首先我們會取得所選頻道的播放清單 ID 。接著我們會指定剛才提過的參數,以便產生正確的 URL 字串,接著我們會建立 NSURL 物件,以便發出要求:

func getVideosForChannelAtIndex(index: Int!) {
    // 從 channelsDataArray 陣列取得所選頻道的 playlistID 數值,並用來擷取正確的影片播放清單
    let playlistID = channelsDataArray[index]["playlistID"] as! String

    // 產生要求 URL 的字串
    let urlString = "https://www.googleapis.com/youtube/v3/playlistItems?part=snippet&playlistId=\(playlistID)&key=\(apiKey)"

    // 根據上述字串產生 NSURL 物件
    let targetURL = NSURL(string: urlString)
}

跟你想像的一樣,頻道的播放清單應該會包含許多部影片,不過在本文中,我們並不需要取得所有的影片。我們只需要取得一部影片就夠了。不過在本例中,我們並不需要明確地限制結果,因為 YouTube API 預設一次只會傳回 5 筆結果。

下列是傳回的 JSON 結果的範例。跟之前看過的一樣,這份結果有助於明確地指引我們應該存取哪些數值:

{
 "kind": "youtube#playlistItemListResponse",
 "etag": "\"Y3xTLFF3RLtHXX85JBgzzgp2Enw/ep-DtNxjJwMQbpCO1Lk3_ggMScU\"",
 "nextPageToken": "CAUQAA",
 "pageInfo": {
  "totalResults": 1636,
  "resultsPerPage": 5
 },
 "items": [
  {

   "kind": "youtube#playlistItem",
   "etag": "\"Y3xTLFF3RLtHXX85JBgzzgp2Enw/SYrDBZ2Ywgpf3zgCreEdB4PIf1o\"",
   "id": "UUZwDRPIG5DD2lxeCjap51NdbKiDO_M62c",
   "snippet": {
    "publishedAt": "2015-06-25T01:50:54.000Z",
    "channelId": "UCK8sQmJBp8GCxrOtXWBpyEA",
    "title": "The Google app: Summer",
    "description": "\"OK Google, when is Summer over?\"\n\nTalk to Google to get answers, find stuff nearby, and get things done. The Google app. Available on iOS and Android. \n\nDownload the app here: http://www.google.com/search/about/download/",
    "thumbnails": {
     "default": {
      "url": "https://i.ytimg.com/vi/BVGKskYZrw8/default.jpg",
      "width": 120,
      "height": 90
     },
     "medium": {
      "url": "https://i.ytimg.com/vi/BVGKskYZrw8/mqdefault.jpg",
      "width": 320,
      "height": 180
     },
     "high": {
      "url": "https://i.ytimg.com/vi/BVGKskYZrw8/hqdefault.jpg",
      "width": 480,
      "height": 360
     },
     "standard": {
      "url": "https://i.ytimg.com/vi/BVGKskYZrw8/sddefault.jpg",
      "width": 640,
      "height": 480
     },
     "maxres": {
      "url": "https://i.ytimg.com/vi/BVGKskYZrw8/maxresdefault.jpg",
      "width": 1280,
      "height": 720
     }
    },
    "channelTitle": "Google",
    "playlistId": "UUK8sQmJBp8GCxrOtXWBpyEA",
    "position": 0,
    "resourceId": {
     "kind": "youtube#video",
     "videoId": "BVGKskYZrw8"
    }
   }
  },
  ... 更多項目 ...
 ]
}

針對影片項目,在傳回的所有數值中,我們只需要保留 titledefault thumbnailvideoID 數值。我們稍後將會使用 videoID 來播放影片串流。

程式碼如下所示:

func getVideosForChannelAtIndex(index: Int!) {
    ...

    // 從 Google 取得播放清單
    performGetRequest(targetURL, completion: { (data, HTTPStatusCode, error) -> Void in
        if HTTPStatusCode == 200 && error == nil {
            do {
                // 將 JSON 資料轉換成字典
                let resultsDict = try NSJSONSerialization.JSONObjectWithData(data!, options: []) as! Dictionary
                
                // 取得所有的播放清單項目(形成 items 陣列)
                let items: Array> = resultsDict["items"] as! Array>
                
                // 利用迴圈來處理所有的影片項目
                for var i=0; i)["snippet"] as! Dictionary
                    
                    // 初始化新的字典,並且儲存感興趣的資料
                    var desiredPlaylistItemDataDict = Dictionary()
                    
                    desiredPlaylistItemDataDict["title"] = playlistSnippetDict["title"]
                    desiredPlaylistItemDataDict["thumbnail"] = ((playlistSnippetDict["thumbnails"] as! Dictionary)["default"] as! Dictionary)["url"]
                    desiredPlaylistItemDataDict["videoID"] = (playlistSnippetDict["resourceId"] as! Dictionary)["videoId"]
                    
                    // 將 desiredPlaylistItemDataDict 字典附加到影片陣列中
                    self.videosArray.append(desiredPlaylistItemDataDict)
                    
                    // 重新載入表格視圖
                    self.tblVideos.reloadData()
                }
            } catch {
                print(error)
            }
        }
        
    })

}

我們用來「抽取」所需數值的邏輯,跟之前剖析頻道時的很類似,所以在此就不贅述了。背後的概念非常簡單;我們根據的是範例結果,並據此修改我們的程式碼。

請留意,一旦關於影片的資料被新增到陣列之後,我們便必須重新載入表格視圖以便更新 UI 。

最後,讓我們幫此函式收尾:

func getVideosForChannelAtIndex(index: Int!) {
    ...

    // 從 Google 擷取播放清單
    performGetRequest(targetURL, completion: { (data, HTTPStatusCode, error) -> Void in
        if HTTPStatusCode == 200 && error == nil {
            ...
        }
        else {
            println("HTTP Status Code = \(HTTPStatusCode)")
            println("Error while loading channel videos: \(error)")
        }

        // 隱藏活動指示器
        self.viewWait.hidden = true
    })
}

我們只會顯示 HTTP 狀態代碼,以及任何遭遇的錯誤(例如沒有可以剖析的資料),並再次隱藏 viewWait 視圖。

顯示影片

現在要做是將擷取到的影片清單顯示出來,動作同樣非常簡單,就跟我們在顯示頻道時做過的一樣,只要把對應的項目呈現出來就可以了。那麼,首先我們必須指定表格視圖的資料列數目:

func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    if segDisplayedContent.selectedSegmentIndex == 0 {
        return channelsDataArray.count
    }
    else {
        return videosArray.count
    }
}

資料列數目顯然必須與 videosArray 陣列中存在的影片字典數目相符。

接著,讓我們來處理儲存格的部分,我們將首次使用 tag 數值來存取儲存格的個別子視圖,然後替它們設定影片的標題和縮圖。相較之下,頻道只有一個用來顯示影片標題的標籤物件,而沒有簡介標籤。此外還有用來顯示影片縮圖的影像視圖;同樣地,我們會使用剖析出來的 URL 數值從網際網路即時取得縮圖。

func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    var cell: UITableViewCell!

    if segDisplayedContent.selectedSegmentIndex == 0 {
        cell = tableView.dequeueReusableCellWithIdentifier("idCellChannel", forIndexPath: indexPath)
        ...
    }
    else {
        cell = tableView.dequeueReusableCellWithIdentifier("idCellVideo", forIndexPath: indexPath)

        let videoTitle = cell.viewWithTag(10) as! UILabel
        let videoThumbnail = cell.viewWithTag(11) as! UIImageView

        let videoDetails = videosArray[indexPath.row]
        videoTitle.text = videoDetails["title"] as? String
        videoThumbnail.image = UIImage(data: NSData(contentsOfURL: NSURL(string: (videoDetails["thumbnail"] as? String)!)!)!)
    }

    return cell
}

現在範例 App 已經能夠下載和顯示所選頻道的影片清單,如果有興趣的話,你也可以實際嘗試看看。不過現在最應該做的,是再幫 App 完成另外一項功能:可以透過分段元件來切換頻道和影片。在 ViewController 類別中,原本就定義了一個名為 changeContent(...) 的 IBAction 函式。每次當你選取不同的區段時都會觸發此函式,其程式碼如下所示:

@IBAction func changeContent(sender: AnyObject) {
    tblVideos.reloadSections(NSIndexSet(index: 0), withRowAnimation: UITableViewRowAnimation.Fade)
}

上述這行將會以淡入淡出的方式來重新載入表格視圖的資料。當分段元件的索引改變時,將會自動選取正確的資料來源,並將正確的內容顯示到表格視圖中。

下圖是 Apple 頻道的影片清單截圖:

youtube-api-videos

搜尋頻道與影片

使用者最常使用到的關鍵功能,就是內容的搜尋,無論是 Google 的哪一款產品或 API ,皆致力於實現搜尋的功能。基於這項事實,我深深感覺到如果不能夠加入利用 YouTube API 來進行搜尋的功能,我們的範例 App 似乎就少了點什麼,所以接下來就讓我們來補齊這個部分吧。一般而言,在 YouTube 中,使用者可以搜尋影片、頻道和播放清單。在本文中,我們只會啟用前 2 項的搜尋功能。

說得更明白一些,我們不會同時搜尋頻道與影片。搜尋的類型( Type )將視所選的分段元件索引而定。也就是說,如果選取的是頻道,那麼我們的「搜尋引擎」將會預設傳回頻道結果。相反地,如果選取的是影片,則會傳回影片結果。

在下載的 Starter 專案中,我已經將 ViewController 類別設定為文字欄位(我們的搜尋欄位)的委派,並且定義了 textFieldShouldReturn(...) 委派函式。此刻我們將著手實作此函式,並從 2 件事情開始:隱藏鍵盤,以及根據分段元件的選取索引來決定搜尋類型。程式碼示範如下:

func textFieldShouldReturn(textField: UITextField) -> Bool {
    textField.resignFirstResponder()
    viewWait.hidden = false

    // 指定搜尋類型(頻道或影片)
    var type = "channel"
    if segDisplayedContent.selectedSegmentIndex == 1 {
        type = "video"
        videosArray.removeAll(keepCapacity: false)
    }

    return true
}

我們要使用的 YouTube API 函式是 search.list ,相關文件在這裡。在我們送出的 GET 要求中,除了 API 金鑰之外,還有 3 個額外參數。第 1 個必要的參數是 part ,其值必須指派為 snippet (以便取得結果項目的詳情)。第 2 個參數是 type ,我們將指派為前面所指定的頻道或影片數值,以便傳回正確的結果。第3 個、同時也是最重要的參數是 q ,也就是實際的搜尋字串(換言之,也就是文字欄位的內容)。缺少了第 3 個參數,便無法實現搜尋功能。

現在讓我們來準備要求的 URL 網址。這回我們增加了一些新的東西,也是我們頭一次實際將要求進行 URL 編碼。這麼做是必要的,好讓所有的特殊字元或空格都被正確置換成百分比逸出字元(例如以 %20 來取代空格字元)。

func textFieldShouldReturn(textField: UITextField) -> Bool {
    ...

    // 產生要求的 URL 字串
    var urlString = "https://www.googleapis.com/youtube/v3/search?part=snippet&q=\(textField.text)&type=\(type)&key=\(apiKey)"
    urlString = urlString.stringByAddingPercentEscapesUsingEncoding(NSUTF8StringEncoding)!

    // 根據上述字串建立 NSURL 物件
    let targetURL = NSURL(string: urlString)

    return true
}

下一步便是發出要求。初始的步驟都非常簡單,亦即取得 JSON 資料並且轉換成字典。接著取得所有的搜尋結果項目,最後建立迴圈逐一處理。

func textFieldShouldReturn(textField: UITextField) -> Bool {
    ...

    // 取得結果
    performGetRequest(targetURL, completion: { (data, HTTPStatusCode, error) -> Void in
        if HTTPStatusCode == 200 && error == nil {
            // 將 JSON 資料轉換成字典物件
            do {
                let resultsDict = try NSJSONSerialization.JSONObjectWithData(data!, options: []) as! Dictionary
                
                // 取得所有的搜尋結果項目( items 陣列)
                let items: Array> = resultsDict["items"] as! Array>
                
                // 以迴圈迭代處理所有的搜尋結果,並且只保留所需的資料
                for var i=0; i
                    
                    // 根據搜尋的目標是頻道或影片來收集正確的資料
                    if self.segDisplayedContent.selectedSegmentIndex == 0 {

                    }
                    else {

                    }
                }
            } catch {
                print(error)
            }
                        
        }
        
    })

    return true
}

上述程式碼並無特別困難之處,所以我們只要先關注頻道結果即可。在本例中,對於結果的處理非常單純,因為我們只需要取得個別頻道的 ID 並將之附加到 desiredChannelsArray 陣列當中。如果你還有印象的話,我們一開始便在此陣列中加入過 Apple 、 Google 和 Microsoft 等頻道名稱了。此外,我們建立了 getChannelDetails(...) 函式,負責存取 API 並且擷取關於頻道的詳細資料。在實作的時候,我們曾經提到,要求可以基於頻道擁有者的使用者名稱,或是頻道本身的 ID ;除此之外,我們也會加上實現此功能的邏輯,並且留待稍後(也就是現在)在使用頻道 ID 來建立要求 URL 字串時再做說明。

所以現在就讓我們將 getChannelDetails(...) 函式缺漏的部分補齊吧。只需要加入下列的 else 分支:

func getChannelDetails(useChannelIDParam: Bool) {
    var urlString: String!
    if !useChannelIDParam {
        ...
    }
    else {
        urlString = "https://www.googleapis.com/youtube/v3/channels?part=contentDetails,snippet&id=\(desiredChannelsArray[channelIndex])&key=\(apiKey)"
    }

    ...
}

現在讓我們來探討搜尋結果的剖析,並且在 desiredChannelsArray 陣列中加入擷取到的頻道 ID 。請留意,除了這些動作之外,在 for 迴圈之後立即呼叫了 getChannelDetails(...) 函式,並且傳入 true 數值作為參數,指示必須根據頻道 ID 來產生該要求:

func textFieldShouldReturn(textField: UITextField) -> Bool {
    ...

    // 取得結果
    performGetRequest(targetURL, completion: { (data, HTTPStatusCode, error) -> Void in
        if HTTPStatusCode == 200 && error == nil {
            ...

            // 以迴圈迭代處理所有的搜尋結果,並且只保留所需的資料
            for var i=0; i

                // 根據搜尋的目標是頻道或影片來收集正確的資料
                if self.segDisplayedContent.selectedSegmentIndex == 0 {
                    // 記住頻道 ID
                    self.desiredChannelsArray.append(snippetDict["channelId"] as! String)
                }
                else {

                }
            }

            // 呼叫 getChannelDetails(...) 函式以便擷取頻道
            if self.segDisplayedContent.selectedSegmentIndex == 0 {
                self.getChannelDetails(true)
            }
        }
    })

    return true
}

在完成上述的步驟之後,便準備好可以搜尋頻道了。搜尋所傳回的任何新頻道,都將被新增到表格視圖中既有的項目之後。請留意,在預設的情況下,每次要求只會傳回 5 筆結果,這樣對我們而言就夠了,我們也省得設定任何限制或篩選器。

t39_8_search_channels_sample

現在讓我們來處理影片搜尋的結果。在本例中,我們將會建立新的字典,用來存放標題、縮圖 URL 和影片的 ID 。接著,此字典會被附加到 videosArray 陣列當中,並且重新整理表格視圖:

func textFieldShouldReturn(textField: UITextField) -> Bool {
    ...

    // 取得結果
    performGetRequest(targetURL, completion: { (data, HTTPStatusCode, error) -> Void in
        if HTTPStatusCode == 200 && error == nil {
            ...

                // 以迴圈迭代處理所有的搜尋結果,並且只保留所需的資料
                for var i=0; i
                    
                    // 根據搜尋的目標是頻道或影片來收集正確的資料
                    if self.segDisplayedContent.selectedSegmentIndex == 0 {
                        ...
                    }
                    else {
                        // 建立新的字典,用來儲存影片詳情
                        var videoDetailsDict = Dictionary()
                        videoDetailsDict["title"] = snippetDict["title"]
                        videoDetailsDict["thumbnail"] = ((snippetDict["thumbnails"] as! Dictionary)["default"] as! Dictionary)["url"]
                        videoDetailsDict["videoID"] = (items[i]["id"] as! Dictionary)["videoId"]
                        
                        // 將 desiredPlaylistItemDataDict 字典附加到影片陣列當中
                        self.videosArray.append(videoDetailsDict)
                        
                        // 重新載入表格視圖
                        self.tblVideos.reloadData()
                    }
                }

            ...
        }
    })

    return true
}

現在,我們只差還沒有加入錯誤處理以及隱藏 viewWait 視圖的程式碼而已了。

func textFieldShouldReturn(textField: UITextField) -> Bool {
    ...

    // 取得結果
    performGetRequest(targetURL, completion: { (data, HTTPStatusCode, error) -> Void in
        if HTTPStatusCode == 200 && error == nil {
            ...            
        }
        else {
            println("HTTP Status Code = \(HTTPStatusCode)")
            println("Error while loading channel videos: \(error)")
        }

        // 隱藏活動指示器
        self.viewWait.hidden = true
    })


    return true
}

讓我們再度嘗試執行此 App ,這回要測試的是影片搜尋的功能。

t39_9_search_videos_sample

播放影片

在 iOS App 中播放 YouTube 影片非常容易,因為 Google 提供了能夠用來嵌入播放器並且接收影片內容串流的輔助( Helper )程式庫。此輔助類別會建立內嵌 iframe 的特殊網頁視圖,使得整合變得非常容易。你可以在這裡找到對應的文件。

針對 iOS ,此程式庫是以 Objective-C 撰寫而成,所以要在 Swift App 中使用的話,必須建立所需的表頭橋接檔案。為了方便說明並且節省時間起見,我已經在下載的 Starter 專案建立好此檔案,同時也加入了程式庫的必要檔案。說得更清楚一些,針對專案中的影片輔助程式庫,你會看到有下列這些東西:

  1. 名為 Assets 的群組
  2. YTPlayerView.h 檔案
  3. YTPlayerView.m 檔案

除此之外,在 Interface Builder 的 Player View Controller 場景中,我加入了一個視圖物件作為子視圖。此視圖的類別已設定為 YTPlayerView ,至此所有的初始步驟便大功告成。現在,我們只需要撰寫少許的程式碼,讓播放器能夠顯示 YouTube 影片的串流即可。

假使你還記得的話,在擷取和剖析影片資料期間,我們只儲存了特定的數值。其中之一便是 video ID ,而現在正是拿來使用的時刻。我們會將此數值提供給 YTPlayerView ,然後我們的專案也就差不多完成了。就讓我們從在 ViewController 類別中點擊影片儲存格開始。當此動作發生時,將會完成 2 項任務:首先,從 videosArray 陣列中取得正確的影片 ID ,接著載入 PlayerViewController 視圖控制器。與此同時,其 Segue 也蓄勢待發,我們會將影片 ID 傳遞給下一個視圖控制器。

再度回到動作函式,首先來到 ViewController 類別的開頭,並且宣告如下的變數(最後一個了):

var selectedVideoIndex: Int!

接著,更新下列的表格視圖委派函式:

func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
    if segDisplayedContent.selectedSegmentIndex == 0 {
        ...
    }
    else {
        selectedVideoIndex = indexPath.row
        performSegueWithIdentifier("idSeguePlayer", sender: self)
    }
}

看吧,一點都不難,我們只需要記住點擊資料列的索引,然後執行 Segue 。現在讓我們來檢視 Segue 的準備程式碼,實際上我們是存取影片 ID 並將之傳遞給 PlayerViewController 類別。

override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
    if segue.identifier == "idSeguePlayer" {
        let playerViewController = segue.destinationViewController as! PlayerViewController
        playerViewController.videoID = videosArray[selectedVideoIndex]["videoID"] as! String
    }
}

如果 Xcode 顯示錯誤的話請勿驚慌。那是因為 videoID 屬性尚未存在於 PlayerViewController 類別當中,不過我們現在就會立刻來修正此問題。

開啟 PlayerViewController 類別,並且加入下列的宣告:

var videoID: String!

最後,在 viewDidLoad 中呼叫 YTPlayerView 類別的函式:

override func viewDidLoad() {
    super.viewDidLoad()

    playerView.loadWithVideoId(videoID)
}

我們準備好了。現在可以執行此 App ,選擇並觀賞任何你喜歡的影片。你應該也注意到了,播放器允許全螢幕,此外也可以執行一些其他動作。

結語

要使用 YouTube API 一點也不難,只需要找到 Google 所提供的正確資源和文件。針對每個要求,都有詳細的文件和範例能夠引導你完成實作,讓你能夠按照最正確的方式來取得和處理資料。如同我在引言當中所說的,在本文中我們所發出的要求只會需要用到簡單的 API 金鑰,無須執行使用者授權程序。但是有很多情況是需要使用者在執行特定動作(例如上傳影片)之前先完成登入。如你所知,我們無法只用單篇文章來涵蓋所有這些主題,不過你在本文的所學已經足以讓你開始使用 YouTube API 了。你當然也可以運用類似於前面幾個小節所介紹的方法來使用其他的 API ,所以請你挑一組喜歡的 API 盡情嘗試吧!

供你參考,請從 Github 下載本文的完整 Xcode 專案。

譯者簡介:陳佳新 – 奇步應用共同創辦人,開發自有 App 和網站之外,也承包各式案件。譯有多本電腦書籍,包括 O'Reilly 出版的 iOS 、 Android 、 Agile 和 Google Cloud 等主題,也在報紙上寫過小說。現與妻兒居住在故鄉彰化。歡迎造訪 https://chibuapp.com ,來信請寄到 [email protected]

原文Building a Video Search App with YouTube API


資深軟體開發員,從事相關工作超過二十年,專門在不同的平台和各種程式語言去解決軟體開發問題。自2010年中,Gabriel專注在iOS程式的開發,利用教程與世界上每個角落的人分享知識。可以在Google+或推特關注 Gabriel。

blog comments powered by Disqus
Shares
Share This