#5 YouTube Data API|詹姆士姆士流

偶爾喜歡在廚房搞東搞西的我,除了從食譜、IG找靈感外,也會看YouTube上各種神人的分享,其中一位就是詹姆士大神,很喜歡他說過的一句話:「吃飯,可以一個人吃,但是不要隨便。」

學習目標

  1. 使用YouTube API取得影片資料
  2. 利用 URLSession 抓取後台的 JSON 資料
  3. 利用 JSONDecoder 和 Codable 將 JSON 資料生成自訂型別
  4. 以TableViewController顯示回傳的資料,點選cell連結至YouTube影片
  5. 影片like按鈕功能
  6. Segmented Control應用

StoryBoard架構

YouTube API抓資料

1. 先從videos list API取得channel ID

以下videos API 可取得影片蘑菇醬鐵板麵的相關資訊,影片的 id 為 BTgIPEhFvfo,且必須包含key=自己申請的金鑰

https://youtube.googleapis.com/youtube/v3/videos?part=snippet&id=BTgIPEhFvfo&t=12s&key=[YOUR_API_KEY]
影片id為橘色畫線處

當part=snippet 時,JSON包含以下內容,可取得channel id為UC4_Ds33FmwzcTDL3G2pbs7g

2. 再從channel list API裡取得playlists ID

以下channel API 的 id 為上一步驟取得的channel idUC4_Ds33FmwzcTDL3G2pbs7g,一樣必須包含key=自己申請的金鑰

https://youtube.googleapis.com/youtube/v3/channels?part=contentDetails&id=UC4_Ds33FmwzcTDL3G2pbs7g&key=[YOUR_API_KEY]

當part=contentDetails時,JSON包含以下內容,可從uploads 欄位取得 playlist 的 id 為 UU4_Ds33FmwzcTDL3G2pbs7g

3. 最後從playlistItems list API取得 playlist 的影片清單

以下playlistItems API 的 id 為上一步驟取得的playlist idUU4_Ds33FmwzcTDL3G2pbs7g,maxResults 代表回傳的影片數量,可指定的最大數字為 50 (預設值為5),一樣必須包含key=自己申請的金鑰

https://youtube.googleapis.com/youtube/v3/playlistItems?part=snippet&maxResults=50&playlistId=UU4_Ds33FmwzcTDL3G2pbs7g&key=[YOUR_API_KEY]

當part=snippet時,JSON包含以下內容,可看到items包含了50筆array資料,可查詢影片相關資訊。

從上方JSON的內容可以得知總影片數量是427筆,但因為maxResults一次最多只能抓取50筆資料,因此可以每一次得到的nextPageToken來取得下一頁的50筆資料,直到抓取完所有資料。
此次我只抓取50筆資料,若要抓取下一頁的50筆資料,只要在 playlistItems list API 裡將參數 pageToken指定為上方nextPageToken 的 EAAaBlBUOkNESQ,就可以抓取第二頁的50筆資料。

https://youtube.googleapis.com/youtube/v3/playlistItems?part=snippet&maxResults=50&playlistId=UU4_Ds33FmwzcTDL3G2pbs7g&key=[YOUR_API_KEY]&pageToken=EAAaBlBUOkNESQ

🌟備註:將從YouTube Data API取得的API,貼到Postman檢查後台功能是否正常,並將自己申請的金鑰填入,以取得回傳的JSON。

程式撰寫

🔸透過Codable將JSON物件轉換成自訂的型別

import Foundation

//透過Codable將影片清單的JSON資料轉成自訂型別
struct SearchResponse: Codable {
let nextPageToken: String
let items: [Item]

struct Item: Codable {
let snippet: Snippet

struct Snippet: Codable {
let title: String
let description: String
let thumbnails: Thumbnail
let resourceId: ResourceID

struct Thumbnail: Codable {
let medium: ThumbnailImage
let standard: ThumbnailImage
let maxres: ThumbnailImage

struct ThumbnailImage: Codable {
let url: URL
}
}

struct ResourceID: Codable {
let videoId: String
}
}
}
}

🔸自訂型別儲存要在TableViewController上呈現的影片資料

import Foundation

//建立表格要呈現的影片資料,包含:影片縮圖、標題、影片ID與isFavorite四個變數。
struct Video {
var thumbnailUrl: URL
var title: String
var videoID: String
var isFavorite: Bool = false //預設為false
}

(James)TableViewCell

→ Table View Cell設定ID,style選擇custom

→ 將要顯示於cell中的元件拉@IBOutlet 在Table View Cell

(James)TableViewController

→ 定義抓資料的fetchItems

//定義抓資料的fetchItems
func fetchItems() {
let apikey = "[YOUR_API_KEY]"
//let nextPageToken = "EAAaBlBUOkNESQ"

let urlString = "https://youtube.googleapis.com/youtube/v3/playlistItems?part=snippet&maxResults=50&playlistId=UU4_Ds33FmwzcTDL3G2pbs7g&key=[YOUR_API_KEY]"
if let url = URL(string: urlString) {
//{}的程式是closure,資料下載完成時會執行{}的程式,傳入data(抓到的資料),reponse(後台回傳抓資料的結果),error(錯誤資訊)三個參數。
URLSession.shared.dataTask(with: url) { data, response, error in
if let data {
let decoder = JSONDecoder()
do {
//透過decode將Data轉成對應的物件內容
let searchResponse = try decoder.decode(SearchResponse.self, from: data)
// 將解碼後的資料取出需要的項目整理到新的array中
var newvideos: [Video] = []
for item in searchResponse.items {
let video = Video(thumbnailUrl: item.snippet.thumbnails.standard.url , title: item.snippet.title, videoID: item.snippet.resourceId.videoId)
newvideos += [video]
}
//因為completionHandler將在function dataTask執行過後一段時間才執行,所以ul4ur8
self.allVideos = newvideos
//需在main thread執行UI相關的程式
DispatchQueue.main.async {
//如果沒有reload data,表格不會更新,只會看到一片空白
self.tableView.reloadData()
}
print("get data")
} catch {
print(error)
}
}
} .resume()
print("function dataTask執行完會先回傳task,然後呼叫task的resume啟動它")
}

}

→ 建立array存放資料

//宣告allVideos變數儲存所有影片
var allVideos = [Video]()
//宣告favoriteVideos變數儲存isFavorite為true的影片
var favoriteVideos = [Video]()

→ segmentedControl拉@IBOutlet與@IBAction,當點選segmentedControl時,會更新表格,分為顯示全部影片或顯示喜愛的影片。

→ 點選cell的likeButton(愛心)時,將影片加入喜愛項目,並用reloadRows來更新被點選的cell內容。

  //點選likeButton時,將影片加入喜愛項目,並更新點選的cell內容
@IBAction func likeAction(_ sender: UIButton) {
//更新allVideos的isFavorite狀態
allVideos[sender.tag].isFavorite = !allVideos[sender.tag].isFavorite
//更新指定的cell(at indexPaths: [IndexPath],with animation: UITableView.RowAnimation)
tableView.reloadRows(at: [IndexPath(row: sender.tag, section: 0)], with: .none)
}

→ 表格顯示資料

  • 表格有幾段 (此次只有一段)
//表格有幾段
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
  • 每一段表格有幾列
    當segmentedControl選在0時,顯示所有抓到的影片 ;
    當segmentedControl選在1時,顯示喜愛的影片(isFavorite為true)
//每一段表格有幾列
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
//當segment為0時,顯示所有抓到的影片
if segmentedControl.selectedSegmentIndex == 0 {
return allVideos.count
//當segment為1時,顯示有點選isFavorite的影片
} else {
//array過濾函示filter,可以把$0看成array的元素
favoriteVideos = allVideos.filter({ $0.isFavorite})
return favoriteVideos.count
}
}
  • 回傳哪一個cell,可從參數 indexPath 得到 section & row
//回傳哪一個cell,可從參數 indexPath 得到 section & row
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

let cell = tableView.dequeueReusableCell(withIdentifier: "JamesTableViewCell", for: indexPath) as! JamesTableViewCell

//當segment為0時,顯示所有抓到的影片; 當segment為1時,顯示有點選isFavorite的影片
var video: Video
//也可以這樣寫:var video = allVideos[indexPath.row]
if segmentedControl.selectedSegmentIndex == 0 {
video = allVideos[indexPath.row]
} else {
video = favoriteVideos[indexPath.row]
}

//顯示影片標題
cell.titleLabel.text = video.title

//用三元運算子來簡化條件判斷式的寫法 (likeButton顯示的image)
cell.likeButton.imageView?.image = video.isFavorite ? UIImage(systemName: "heart.fill") : UIImage(systemName: "heart")
//原本條件判斷式寫法 (likeButton顯示的image)
/*
var isFavoriate = true
if isFavoriate {
cell.likeButton.imageView?.image = UIImage(systemName: "heart.fill")
} else {
cell.likeButton.imageView?.image = UIImage(systemName: "heart")
}
*/

//用tag辨別點選了哪一個按鈕
cell.likeButton.tag = indexPath.row

//先顯示預設圖片,才不會因從網路抓取需時間而看到先前的圖片
cell.thumbnailImageView.image = UIImage(systemName: "video.circle.fill")
URLSession.shared.dataTask(with:
video.thumbnailUrl) { data, response, error in
if let data {
let image = UIImage(data: data)
DispatchQueue.main.async {
cell.thumbnailImageView.image = image
}
}
}.resume()

return cell
}

→ 以prepare 傳資料到下一頁(Video)View Controller

 //以prepare傳遞資料到下一頁
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let videoVC = segue.destination as? VideoViewController {
let selectedRow = tableView.indexPathForSelectedRow
if let passIndex = selectedRow?.row {
videoVC.index = passIndex
videoVC.data = allVideos
}
}
}
TableViewCell拉segue到下一頁

(Video)ViewController

→ 點選影片後,在此頁面顯示被點選的youtube影片,在頁面上加入WebKit View,並宣告儲存要顯示資料的屬性。

 //前一頁select到的tableview的row的位置
var index: Int!
//宣告儲存前一頁解析完成的API資料
var data = [Video]()

→ 此頁需import Webkit,並將WebKit View拉@IBOutlet

→ 使用WebKit View播放YouYube

override func viewDidLoad() {
super.viewDidLoad()

//透過URLRequest指派並透過JamesWebView顯示網頁
if let url = URL(string: "https://www.youtube.com/watch?v=\(data[index].videoID)") {
let request = URLRequest(url: url)
JamesWebView.load(request)
}
}

遇到的困難

Youtube Data API屬於層層包裹的資料,一開始花了滿多時間研究,但其實不難取得。覺得最困難的部分是將取得的資料呈現在表格上,並且此次學習Chia學姐加入segmentedControl與喜愛button的應用,雖然還是有比較模糊的地方,但有做出想要的樣子,相信不懂的部分會在某一天茅塞頓開的!

🌟小筆記-如何將網路上抓下來的JSON 資料,從Data型別變成自訂型別🌟

  1. 將JSON裡以{ }描述的object變成自訂型別,可用class或struct。
  2. 讓自訂型別遵從protocol Codable或Decodable或Encodable。
  3. JSON裡object的key將成為自訂型別的property名字。
  4. 只要定義App需要的欄位就好,不用將JSON object的key都寫出來。
  5. JSON object不一定有的key某個key對應的value可能是null,property要宣告為optional。

GitHub

Reference

--

--