網路基礎-影音篇
-
連接網路資料庫
在第2單元第7課曾提過,若程式學會解碼JSON格式,就像多出一隻手延伸到網路上的資料庫,可源源不絕取得資料更新網頁,本課就來實現這個想法。
當然,前提是網路上的伺服器必須開放權限,允許取用資料庫才行。
能夠使用JSON解碼器連接網路資料庫,對程式非常有用,因為網路上有非常多JSON格式的開放資料,這類網站提供公開的程式連結,通常稱為 "Open API"。
Apple公司的 iTunes 資料庫就是一個對任何人開放的音樂資料庫,可以傳回 JSON 格式的資料,再用程式加以解碼,就可獲得全世界音樂藝人的相關資料。例如,用瀏覽器連接網址:
https://itunes.apple.com/search?term=Justin+Bieber&media=music
我們可以在程式中搜尋任意歌手的相關資料,本單元我們就來試試看。
在 iTunes 資料庫搜尋小賈斯汀 "Justin Bieber",傳回一個 JSON 格式的檔案,共有50筆資料,內容如下:
{ "resultCount":50, "results": [ { "wrapperType":"track", "kind":"song", "artistId":259760619, "collectionId":359966550, "trackId":359966562, "artistName":"Sean Kingston & Justin Bieber", "collectionName":"Eenie Meenie - Single", "trackName":"Eenie Meenie", "collectionCensoredName":"Eenie Meenie - Single", "trackCensoredName":"Eenie Meenie", "artistViewUrl":"https://music.apple.com/us/artist/sean-kingston/259760619?uo=4", "collectionViewUrl":"https://music.apple.com/us/album/eenie-meenie/359966550?i=359966562&uo=4", "trackViewUrl":"https://music.apple.com/us/album/eenie-meenie/359966550?i=359966562&uo=4", "previewUrl":"https://audio-ssl.itunes.apple.com/itunes-assets/AudioPreview125/v4/bb/ce/30/bbce3088-f26c-f5e9-b8af-50344243726e/mzaf_17828995580283253999.plus.aac.p.m4a", "artworkUrl30":"https://is3-ssl.mzstatic.com/image/thumb/Music125/v4/56/f9/aa/56f9aa10-f1f3-dd77-76d4-2d979030b6fd/source/30x30bb.jpg", "artworkUrl60":"https://is3-ssl.mzstatic.com/image/thumb/Music125/v4/56/f9/aa/56f9aa10-f1f3-dd77-76d4-2d979030b6fd/source/60x60bb.jpg", "artworkUrl100":"https://is3-ssl.mzstatic.com/image/thumb/Music125/v4/56/f9/aa/56f9aa10-f1f3-dd77-76d4-2d979030b6fd/source/100x100bb.jpg", "collectionPrice":1.29, "trackPrice":1.29, "releaseDate":"2010-03-19T07:00:00Z", "collectionExplicitness":"notExplicit", "trackExplicitness":"notExplicit", "discCount":1, "discNumber":1, "trackCount":1, "trackNumber":1, "trackTimeMillis":201880, "country":"USA", "currency":"USD", "primaryGenreName":"Pop", "isStreamable":true}, ....<省略> ] }
想要寫JSON程式,首先必須解析資料的欄位結構。還記得2-7課提到,在JSON格式中,大括號 { } 裡面是一個物件實例,物件實例的每個欄位為 key: value 的形式,欄位之間或陣列元素之間以逗號隔開。陣列則包含在中括號 [ ] 之中。
所以根據上面回傳的JSON內容,有兩層結構,外層是大的物件實例,只有兩個欄位:
struct 頁面結構 { let resultCount: Int let results: [單項] }
第二個欄位 results 是一個陣列,陣列元素「單項」則是另一層資料結構,有31個欄位。根據這樣的欄位解析,就可以來寫一個傑森解碼器,透過網路抓取 iTunes 資料庫。
操作步驟如下:
- 選取畫面左方的「+」號新增電子書頁面。
- 將新增的電子書面頁命名為「(19)網路程式基礎-3影音篇」。
3-3a JSON by URLSession
- 在「Main」模組中撰寫程式:
// 3-3a JSON by URLSession // Created by Philip, Heman, Jean 2021/09/03 // Revised by Jean 2024/11/24 // https://developer.apple.com/library/archive/documentation/AudioVideo/Conceptual/iTuneSearchAPI/index.html import PlaygroundSupport import SwiftUI let 網址 = "https://itunes.apple.com/search?term=Taylor+Swift&media=music" struct 頁面結構: Codable { let resultCount: Int let results: [單項] } struct 單項: Codable, Hashable { let wrapperType: String //track, collection, artist let kind: String //book, album, coached-audio, feature-movie, ... let artistId: Int let collectionId: Int let trackId: Int let artistName: String let collectionName:String let trackName:String } struct 更新頁面: View { @State var 歌曲列表: [單項]? func 更新歌曲列表() { guard let myURL = URL(string: 網址) else { return } URLSession.shared.dataTask(with: myURL) { 回傳資料, 回傳碼 , 錯誤碼 in print(回傳碼) if let 解碼資料 = 回傳資料 { do { let 解碼結果 = try JSONDecoder().decode(頁面結構.self, from: 解碼資料) print(回傳碼 ?? "No response") 歌曲列表 = 解碼結果.results } catch { print("JSON解碼錯誤") } } else { print(錯誤碼 ?? "No error") } }.resume() } var body: some View { if 歌曲列表 == nil { ProgressView() .onAppear { 更新歌曲列表() } } else { List(歌曲列表!, id: \.self) { 歌曲 in Label(歌曲.trackName, systemImage: "music.note") .font(.title) .lineLimit(1) } } } } PlaygroundPage.current.setLiveView(更新頁面())
- 程式執行結果,如下圖。
搜尋iTunes音樂資料庫
能夠使用JSON解碼器連接網路資料庫,對程式非常有用,因為網路上有非常多JSON格式的開放資料,這類網站提供公開的程式連結,通常稱為 "Open API"。
上面的範例,我們用的「網址」其實就是 iTunes 提供的 Open API,透過這個 API,我們可以在程式中搜尋任意歌手的相關資料,以下範例我們就來試試看。
跟上面範例程式比較起來,想要搜尋任意歌手,還必須實現兩個新功能:
(1) 讓使用者輸入歌手名稱
(2) 將歌手名稱加入到API網址中第一個新功能,用到一個視圖稱為 TextField,會顯示一個輸入字串的方框,用法如下:
TextField("歌手", text: $inputText) { 歌手 = inputText 歌曲列表 = nil }
TextField視圖會顯示一個方框,讓使用者輸入字串,如下圖中間的深色框。
TextField() 有兩個參數,"歌手"是方框內的提示文字,第二個參數很關鍵,text: $inputText 是接受輸入值的變數,必須是一個狀態變數(用@State var宣告),使用者輸入的字串,會指定到 inputText 這個變數。
比較特別的地方,是這個變數必須加上金錢符號 $,主要目的是讓參數能夠帶值出來。因為平常沒有加 $ 的參數,只能帶值進去(術語稱為 "Call by value"),加上 $ 才變成 "Call by reference",能夠帶值出來。
此處也是目前唯一不能用中文命名的地方,可能是 Swift Playgrounds 的 bug,但目前就是這樣,所有要用 $ (Call by reference)的變數,只能用英文命名。
當使用者輸入歌手名稱的字串,並按下 ENTER 之後,就會進入 { } 段落裡面,因為鍵盤輸入也是非同步事件,所以 { } 其實是一個沒有參數的匿名函式。在匿名函式中,我們將輸入的字串值指定給「歌手」,並將「歌曲列表」重新設為未初始化的狀態(nil),這樣在更新畫面時才會重新下載歌手資料。
上圖整個橘色搜尋框,用SwiftUI視圖來表現的程式碼如下:
ZStack { Rectangle() .foregroundColor(.orange) .frame(height: 60) HStack { Image(systemName: "magnifyingglass") .font(.title) TextField("歌手", text: $inputText) { 歌手 = inputText 歌曲列表 = nil } .font(.title) .background(Color.secondary) } .padding() }
第二個新功能是將歌手名稱加入到網址的一部分,需用到另一個 Foundation 的物件,稱為 URLComponents,其實就是將 URL 物件拆解開來,用法如下,非常直觀:
var myURLComponent = URLComponents() myURLComponent.scheme = "https" myURLComponent.host = "itunes.apple.com" myURLComponent.path = "/search" myURLComponent.query = "term=\(歌手)&media=music" guard let myURL = myURLComponent.url else { return }
還記得在第一課「網址篇」學習過 URL 的4個部分:scheme(網路種類), host(主機名稱), path(路徑), query(查詢參數),在這個地方,就是依照這4個部分分別指定字串值,尤其是 query 的部分要插入歌手名稱。
此處query(查詢參數)有兩個參數,一是 term=歌手名稱,另一是固定的 media=music,參數之間用 & 符號隔開。在第一課曾經提過,整個字串不能有空格或中文等特殊符號,但是使用者可能輸入空格或查詢中文的歌手名稱怎麼辦?
幸好 URLComponents 會幫我們轉換,如果我們輸入歌手「徐佳瑩」,URLComponents 會轉成:
term=%E5%BE%90%E4%BD%B3%E7%91%A9
其中 % 開頭的編碼稱為 UTF-8 編碼格式,這是 Unicode 的編碼標準之一,也是網址能夠接受的格式。這樣就解決中文網址的問題了。
整個程式碼如下,其中「單項」的資料結構原本有31個欄位,我們只留部分可能會用到的欄位即可,這樣JSON解碼器同樣沒問題。
3-3b iTunes Search
- 在「Main」模組中撰寫程式:
// 3-3b iTunes Search // Created by Philip, Heman, Jean 2021/09/05 // Revised by Jean 2024/11/24 // https://affiliate.itunes.apple.com/resources/documentation/itunes-store-web-service-search-api/ import PlaygroundSupport import SwiftUI struct 頁面結構: Codable { let resultCount: Int let results: [單項] } struct 單項: Codable, Hashable { let artistName: String let collectionName:String let trackName:String let artistViewUrl: URL let collectionViewUrl: URL let trackViewUrl: URL let previewUrl: URL? let artworkUrl30: URL? let artworkUrl60: URL? let artworkUrl100: URL? } struct 更新頁面: View { @State var inputText = "" @State var 歌手 = "Taylor Swift" @State var 歌曲列表: [單項]? func 更新歌曲列表() { // let 網址 = "https://itunes.apple.com/search?term=Justin+Bieber&media=music" // guard let myURL = URL(string: 網址) else { return } var myURLComponent = URLComponents() myURLComponent.scheme = "https" myURLComponent.host = "itunes.apple.com" myURLComponent.path = "/search" myURLComponent.query = "term=\(歌手)&media=music" guard let myURL = myURLComponent.url else { return } URLSession.shared.dataTask(with: myURL) { 回傳資料, 回傳碼 , 錯誤碼 in if let 解碼資料 = 回傳資料 { do { let 解碼結果 = try JSONDecoder().decode(頁面結構.self, from: 解碼資料) print(回傳碼 ?? "No response") 歌曲列表 = 解碼結果.results } catch { print("JSON解碼錯誤") } } else { print(錯誤碼 ?? "No error") } }.resume() } var body: some View { VStack { ZStack { Rectangle() .foregroundColor(.orange) .frame(height: 60) HStack { Image(systemName: "magnifyingglass") .font(.title) TextField("歌手", text: $inputText) { 歌手 = inputText 歌曲列表 = nil } .font(.title) .background(Color.secondary) } .padding() } if 歌曲列表 == nil { ProgressView() .onAppear { 更新歌曲列表() } } else { List(歌曲列表!, id: \.self) { 歌曲 in Label(歌曲.trackName, systemImage: "music.note") .font(.title) .lineLimit(1) } } } } } PlaygroundPage.current.setLiveView(更新頁面())
程式執行結果,如下圖。
【註解】
API 全名是 Application Programming Interface,中文可稱為「應用程式介面」,基本上就是程式與程式之間的溝通規格,例如作業系統與App之間或是Server程式與Client程式之間的溝通。目前大部分的API都已經物件化,物件內容透過JSON格式交換非常方便。
UTF-8編碼在此不多做說明,有興趣者可參考維基百科 https://zh.wikipedia.org/wiki/UTF-8
本單元範例程式有個小bug,如果輸入的歌手名稱搜尋不到任何資料,就會停留在 ProgressView,無法再搜尋。
狀態變數在宣告時就必須提供初始值,或是設為Optional,這樣作業系統才能觀察其「狀態」(變數值)是否有變化。@State var inputText = "" @State var 歌手 = "Taylor Swift" @State var 歌曲列表: [單項]?
3-3c 播放網路影音
- 在「Main」模組中撰寫程式:
// 3-3c 播放網路影音 // Created by Philip, Heman, Jean 2021/09/28 // Revised by Jean 2024/12/08 import PlaygroundSupport import SwiftUI import AVKit let 網址 = "https://audio-ssl.itunes.apple.com/itunes-assets/AudioPreview125/v4/bb/ce/30/bbce3088-f26c-f5e9-b8af-50344243726e/mzaf_17828995580283253999.plus.aac.p.m4a" let myURL = URL(string: 網址)! let 播放器 = AVPlayer(url: myURL) struct 影音播放: View { var body: some View { VideoPlayer(player: 播放器) .onAppear() { 播放器.play() } } } PlaygroundPage.current.setLiveView(影音播放())
- 程式執行結果,如下圖。
3-3d iTunes Search & Play (NavigationView)
- 在「Main」模組中撰寫程式:
// 3-3d iTunes Search & Play (NavigationView) // Created by Philip, Heman, Jean 2021/09/27 // Revised by Jean 2024/12/08 import PlaygroundSupport import SwiftUI import AVKit struct 頁面結構: Codable { let resultCount: Int let results: [單項] } struct 單項: Codable, Hashable { let artistName: String let collectionName:String let trackName:String let artistViewUrl: URL let collectionViewUrl: URL let trackViewUrl: URL let previewUrl: URL? let artworkUrl30: URL? let artworkUrl60: URL? let artworkUrl100: URL? } struct 試聽頁面: View { @State var 下載圖片: UIImage? var 曲目: 單項 init(_ p: 單項) { 曲目 = p } func 下載() { guard let myURL = 曲目.artworkUrl100 else { return } URLSession.shared.dataTask(with: myURL) { 回傳資料, 回應碼, 錯誤碼 in if let 圖檔 = UIImage(data: 回傳資料!) { print(回應碼 ?? "No response") 下載圖片 = 圖檔 } else { print(錯誤碼 ?? "No error") } }.resume() } var 播放器: AVPlayer { AVPlayer(url: 曲目.previewUrl!) } var body: some View { VStack { Text(曲目.artistName) .font(.largeTitle) Text("專輯:\(曲目.collectionName)") .font(.title) .multilineTextAlignment(.center) if 下載圖片 == nil { ProgressView() .onAppear { 下載() } } else { Image(uiImage: 下載圖片!) .resizable() .scaledToFit() } Text(曲目.trackName) .font(.title2) VideoPlayer(player: 播放器) .onAppear() { 播放器.play() } } } } struct 更新頁面: View { @State var inputText = "" @State var 歌手 = "Taylor Swift" @State var 歌曲列表: [單項]? func 更新歌曲列表() { var myURLComponent = URLComponents() myURLComponent.scheme = "https" myURLComponent.host = "itunes.apple.com" myURLComponent.path = "/search" myURLComponent.query = "term=\(歌手)&media=music" guard let myURL = myURLComponent.url else { return } URLSession.shared.dataTask(with: myURL) { 回傳資料, 回傳碼 , 錯誤碼 in if let 解碼資料 = 回傳資料 { do { let 解碼結果 = try JSONDecoder().decode(頁面結構.self, from: 解碼資料) print(回傳碼 ?? "No response") 歌曲列表 = 解碼結果.results } catch { print("JSON解碼錯誤") } } else { print(錯誤碼 ?? "No error") } }.resume() } var body: some View { VStack { ZStack { Rectangle() .foregroundColor(.orange) .frame(height: 60) HStack { Image(systemName: "magnifyingglass") .font(.title) TextField("歌手", text: $inputText) { 歌手 = inputText 歌曲列表 = nil } .font(.title) .background(Color.secondary) } .padding() } if 歌曲列表 == nil { ProgressView() .onAppear { 更新歌曲列表() } } else { NavigationView { List(歌曲列表!, id: \.self) { 歌曲 in NavigationLink(destination: 試聽頁面(歌曲)) { Label(歌曲.trackName, systemImage: "music.note") .font(.title) .lineLimit(1) } } .navigationTitle("\(歌手)@iTunes") Text("⬅︎左方導覽頁面選擇試聽歌曲") } } } } } PlaygroundPage.current.setLiveView(更新頁面())
- 程式執行結果,如下圖。