// 3-3c 播放網路影音 // Created by Heman, 2021/09/28 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(影音播放())
程式分為兩個步驟:
第一部分,產出一個 AVPlayer() 物件實例,參數為影音檔案的網址(轉換為URL物件):
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)
// 3-3d iTunes Search & Play (NavigationView) // Created by Heman, 2021/09/27 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!) }
在過去寫程式最困難的地方,不是語法或演算法,而在於缺乏資料,大部分資料都是由企業、政府或機構所掌握,每筆資料都有產出成本,不會輕易與人分享,所以個人很難寫出有內容的軟體,就像俗話說「巧婦難為無米之炊」。現在拜 Open API 所賜,每個人從網路上都能免費取得豐富資料,這對程式設計者來說,是非常幸福的事。
在上一課,我們用 Open API 連接iTune 音樂資料庫,在本課則要練習連接「芝加哥藝術博物館」館藏資料庫。
芝加哥藝術博物館(Art Institute of Chicago)成立於1879年,是美國僅次於紐約大都會博物館的藝術殿堂,與著名的芝加哥藝術學院(SAIC, School of the Art Institute of Chicago)是聯屬機構。
在完整的程式碼之前,先看看程式執行的過程。由於博物館限制每分鐘最多連線60次,所以我們將搜尋筆數從100筆改為5筆,從這5筆「作品列表」會發出5次連線取得5筆「作品資訊」,然後再連線5次取得5張圖檔,所以每次搜尋到顯示圖片共連線11次,其實很容易就超過限制,如果要做成App,還是必須跟博物館申請 API Key,才能解開限制。
完整程式碼如下,下一節會再進一步討論:
// 3-4b 搜尋並顯示作品圖片(芝加哥藝術博物館) // Created by Heman, 2021/10/05 import PlaygroundSupport import SwiftUI
// Refer to https://api.artic.edu/api/v1/artworks/search?q=van+gogh&limit=100 struct 搜尋結果: Codable { let pagination: 分頁資訊 let data: [搜尋品項] }
struct 分頁資訊: Codable { let total: Int let limit: Int let offset: Int let total_pages: Int let current_page: Int }
struct 搜尋品項: Codable, Identifiable { let api_link: URL? let id: Int let title: String }
// Refer to https://api.artic.edu/api/v1/artworks/28560 struct 藝術作品: Codable { let data: 作品資訊 let config: 配置資訊 }
struct 作品資訊: Codable, Identifiable { let id: Int let api_link: URL? let title: String let date_display: String let artist_display: String let artist_id: Int let artist_title: String let image_id: String }
struct 配置資訊: Codable { let iiif_url: URL? let website_url: URL? }
// 顯示「作品圖」陣列 struct 作品圖: Identifiable { var id: Int = 0 var 作品名稱: String = "" var 作者: String = "" var 圖檔網址: URL? }
struct 抓圖: View { var myURL: URL @State var 下載圖片: UIImage?
init(_ p: URL) {myURL = p}
func 下載() { URLSession.shared.dataTask(with: myURL) { 回傳資料, 回應碼, 錯誤碼 in if let 圖檔 = UIImage(data: 回傳資料!) { print(回應碼 ?? "No response") 下載圖片 = 圖檔 } else { print(錯誤碼 ?? "No error") } }.resume() }
var body: some View { if 下載圖片 == nil { ProgressView() .onAppear { 下載() } } else { Image(uiImage: 下載圖片!) .resizable() .scaledToFit() } } }
struct 芝加哥藝術博物館: View { @State var inputText = "Van Gogh" @State var 搜尋字串 = "van gogh" @State var 作品列表: [搜尋品項]? @State var 作品圖集: [作品圖] = []
// let 網址 = "https://api.artic.edu/api/v1/artworks/search?q=van+gogh&limit=5" // guard let myURL = URL(string: 網址) else { return } func 更新作品列表(_ 字串參數: String) { var myURLComponent = URLComponents() myURLComponent.scheme = "https" myURLComponent.host = "api.artic.edu" myURLComponent.path = "/api/v1/artworks/search" myURLComponent.query = "q=\(字串參數)&limit=5" 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") 作品列表 = 解碼結果.data for 作品 in 作品列表! { 取得作品資訊(作品.api_link!) } } catch { print("JSON解碼錯誤") } } else { print(錯誤碼 ?? "No error") } }.resume() }
// let 網址 = "https://api.artic.edu/api/v1/artworks/28560" // guard let myURL = URL(string: 網址) else { return } // 圖檔網址 IIIF v2 standard format: // https://www.artic.edu/iiif/2/25c31d8d-21a4-9ea1-1d73-6a2eca4dda7e/full/843,/0/default.jpg func 取得作品資訊(_ myURL: URL) { var 單項作品: 作品資訊? var 目前作品 = 作品圖() var myURLComponent = URLComponents() URLSession.shared.dataTask(with: myURL) { 回傳資料, 回傳碼 , 錯誤碼 in if let 待解碼資料 = 回傳資料 { do { let 尺寸 = "600," let 解碼結果 = try JSONDecoder().decode(藝術作品.self, from: 待解碼資料) print(回傳碼 ?? "No response") 單項作品 = 解碼結果.data 目前作品.id = 單項作品!.id 目前作品.作品名稱 = 單項作品!.title 目前作品.作者 = 單項作品!.artist_display myURLComponent.scheme = "https" myURLComponent.host = "www.artic.edu" myURLComponent.path = "/iiif/2/\(單項作品!.image_id)/full/\(尺寸)/0/default.jpg" myURLComponent.query = "" 目前作品.圖檔網址 = myURLComponent.url 作品圖集 = 作品圖集 + [目前作品] } catch { print("JSON解碼錯誤") } } else { print(錯誤碼 ?? "No error") } }.resume() }
// 3-5a 抓大圖(downloadTask) // Created by Heman, 2021/10/13 import PlaygroundSupport import SwiftUI
struct 抓大圖: View { @State var 下載圖片: UIImage? var 圖片編號: String init(_ p: String) {圖片編號 = p}
func 下載() { let 網址 = "https://www.artic.edu/iiif/2/" + 圖片編號 + "/full/max/0/default.jpg" guard let myURL = URL(string: 網址) else { return } URLSession.shared.downloadTask(with: myURL) { 暫存檔, 回應碼, 錯誤碼 in do { let 回傳資料 = try Data(contentsOf: 暫存檔!) if let 圖檔 = UIImage(data: 回傳資料) { print(回應碼 ?? "No response") 下載圖片 = 圖檔 } else { print(錯誤碼 ?? "No error") } } catch { print("Data I/O error") } }.resume() }
var body: some View { if 下載圖片 == nil { ProgressView() .onAppear { 下載() } } else { Image(uiImage: 下載圖片!) .resizable() .scaledToFit() } } }
// let 網址 = "https://www.artic.edu/iiif/2/cf50f037-5fb2-e197-0e56-3ae701edb3e2/full/max/0/default.jpg" let image_id = "cf50f037-5fb2-e197-0e56-3ae701edb3e2" PlaygroundPage.current.setLiveView(抓大圖(image_id))
螢幕的「解析度」有兩種意義,第一種代表螢幕的畫素(pixel, 或稱像素)多寡,例如 Full HD 代表 1920x1080,也就寬1920畫素,高1080畫素,兩數相乘等於2,073,600,大約200萬畫素。而所謂 8K 螢幕,是指寬7680畫素,高4320畫素,相乘等於33,177,600,約3300萬畫素。
「解析度」的第二種意義,是指畫素密度,通常用每英吋有多少畫素(dpi或ppi)來表示,例如 Apple 手機螢幕的解析度至少都 326dpi 以上,已超過眼睛所能辨識的最大密度。
圖片為11世紀印度濕婆神「宇宙之舞」銅像(Shiva as Lord of the Dance),濕婆是印度教三大主神中,力量最強大的,主導宇宙的滅亡與再生,有眾多化身。圖片來源及說明可參考: https://www.artic.edu/artworks/24548/shiva-as-lord-of-the-dance-nataraja
// 3-5c 整合(芝加哥藝術博物館) // Created by Heman, 2021/10/13 import PlaygroundSupport import SwiftUI
// Refer to https://api.artic.edu/api/v1/artworks/search?q=van+gogh&limit=100 struct 搜尋結果: Codable { let pagination: 分頁資訊 let data: [搜尋品項] }
struct 分頁資訊: Codable { let total: Int let limit: Int let offset: Int let total_pages: Int let current_page: Int }
struct 搜尋品項: Codable, Identifiable { let api_link: URL? let id: Int let title: String }
// Refer to https://api.artic.edu/api/v1/artworks/28560 struct 藝術作品: Codable { let data: 作品資訊 let config: 配置資訊 }
struct 作品資訊: Codable, Identifiable { let id: Int let api_link: URL? let title: String let date_display: String let artist_display: String let artist_id: Int let artist_title: String let image_id: String }
struct 配置資訊: Codable { let iiif_url: String let website_url: String }
// 顯示「作品圖」陣列 struct 作品圖: Identifiable { var id: Int = 0 var 作品名稱: String = "" var 作者: String = "" var image_id: String = "" var 圖檔網址: URL? }
struct 抓圖: View { var myURL: URL @State var 下載圖片: UIImage?
init(_ p: URL) {myURL = p}
func 下載() { URLSession.shared.dataTask(with: myURL) { 回傳資料, 回應碼, 錯誤碼 in if let 圖檔 = UIImage(data: 回傳資料!) { print(回應碼 ?? "No response") 下載圖片 = 圖檔 } else { print(錯誤碼 ?? "No error") } }.resume() }
var body: some View { if 下載圖片 == nil { ProgressView() .onAppear { 下載() } } else { Image(uiImage: 下載圖片!) .resizable() .scaledToFit() } } }
struct 抓大圖: View { @State var 下載圖片: UIImage? @State var 縮放: Bool = false @State var 位移: CGSize = .zero @State var 上次位移: CGSize = .zero var 圖片編號: String init(_ p: String) {圖片編號 = p} func 下載() { let 網址 = "https://www.artic.edu/iiif/2/" + 圖片編號 + "/full/max/0/default.jpg" guard let myURL = URL(string: 網址) else { return } URLSession.shared.downloadTask(with: myURL) { 暫存檔, 回應碼, 錯誤碼 in do { let 回傳資料 = try Data(contentsOf: 暫存檔!) if let 圖檔 = UIImage(data: 回傳資料) { print(回應碼 ?? "No response") 下載圖片 = 圖檔 } else { print(錯誤碼 ?? "No error") } } catch { print("Data I/O error") } }.resume() }
從第一單元到目前為止,我們所有程式都是在 Swift Playgrounds 裡面執行,不過,若想開發跨產品(Mac, iPhone, iPad, Apple Watch, Apple TV)的程式,則必須使用 Xcode,這也是Apple陣營的程式設計師唯一選擇,那麼在 Swift Playgrounds 開發的程式,能不能用在 Xcode 呢?
答案是肯定的,本節我們將範例程式 3-5c 導入 Xcode 執行,看看有何差別。
Xcode 與 Swift Playgrounds 兩者的使用環境有所不同,下表是兩者的簡單比較。
表3-5 Xcode 與 Swift Playgrounds 差異
Xcode
Swift Playgrounds
作業系統
只能在MacOS執行
MacOS 或 iPadOS
硬體設備
Mac (iMac, Macbook, Mac mini)
Mac (iMac, Macbook, Mac mini), iPad
語言版本
只有英文版
支援14種語言(繁中、簡中、英、法、瑞典、義、荷、葡、西、土、德、日、韓、泰)
大小
12.4GB (v13.1)
232MB (v3.4.1)
產出App
所有Apple產品
僅iOS
費用
免費 上傳App Store須繳年費
免費 上傳App Store須繳年費
發布年代
2003年(v1.0)
2016年(v1.0)
所以需要一台Mac才能使用Xcode,本節範例筆者用Macbook Air (M1),花了將近12小時才升級完 MacOS 12.0.1 與 Xcode 13.1。
原來我們在 Swift Playgrounds 寫的程式,第一行與最後一行只在 Swift Playgrounds 裡面才用得到,換到 Xcode 就不需要了。
然後更改 ContentView 主體,改成「芝加哥藝術博物館()」:
struct ContentView: View { var body: some View { 芝加哥藝術博物館() } }
(八)執行預覽
以上執行過程的影片如下:
在 Xcode 上面的完整程式碼如下:
// // ContentView.swift // Shared // // Created by Heman Lu on 2021/10/29. // // 3-5c 整合(芝加哥藝術博物館) // Created by Heman, 2021/10/13 // import PlaygroundSupport import SwiftUI
// Refer to https://api.artic.edu/api/v1/artworks/search?q=van+gogh&limit=100 struct 搜尋結果: Codable { let pagination: 分頁資訊 let data: [搜尋品項] }
struct 分頁資訊: Codable { let total: Int let limit: Int let offset: Int let total_pages: Int let current_page: Int }
struct 搜尋品項: Codable, Identifiable { let api_link: URL? let id: Int let title: String }
// Refer to https://api.artic.edu/api/v1/artworks/28560 struct 藝術作品: Codable { let data: 作品資訊 let config: 配置資訊 }
struct 作品資訊: Codable, Identifiable { let id: Int let api_link: URL? let title: String let date_display: String let artist_display: String let artist_id: Int let artist_title: String let image_id: String }
struct 配置資訊: Codable { let iiif_url: String let website_url: String }
// 顯示「作品圖」陣列 struct 作品圖: Identifiable { var id: Int = 0 var 作品名稱: String = "" var 作者: String = "" var image_id: String = "" var 圖檔網址: URL? }
struct 抓圖: View { var myURL: URL @State var 下載圖片: UIImage?
init(_ p: URL) {myURL = p}
func 下載() { URLSession.shared.dataTask(with: myURL) { 回傳資料, 回應碼, 錯誤碼 in if let 圖檔 = UIImage(data: 回傳資料!) { print(回應碼 ?? "No response") 下載圖片 = 圖檔 } else { print(錯誤碼 ?? "No error") } }.resume() }
var body: some View { if 下載圖片 == nil { ProgressView() .onAppear { 下載() } } else { Image(uiImage: 下載圖片!) .resizable() .scaledToFit() } } }
struct 抓大圖: View { @State var 下載圖片: UIImage? @State var 縮放: Bool = false @State var 位移: CGSize = .zero @State var 上次位移: CGSize = .zero var 圖片編號: String init(_ p: String) {圖片編號 = p} func 下載() { let 網址 = "https://www.artic.edu/iiif/2/" + 圖片編號 + "/full/max/0/default.jpg" guard let myURL = URL(string: 網址) else { return } URLSession.shared.downloadTask(with: myURL) { 暫存檔, 回應碼, 錯誤碼 in do { let 回傳資料 = try Data(contentsOf: 暫存檔!) if let 圖檔 = UIImage(data: 回傳資料) { print(回應碼 ?? "No response") 下載圖片 = 圖檔 } else { print(錯誤碼 ?? "No error") } } catch { print("Data I/O error") } }.resume() }