網路基礎-整合篇
-
一個 URLSession 連線任務可以發出一個或多個連線工作(task),連線工作則分為5種類型:
1. dataTask() -- 一般較短暫的資料下載
2. downloadTask() -- 較長時間的檔案下載
3. uploadTask() -- 上傳資料
4. streamTask() -- 音樂、語音或影片的串流資料下載
5. webSocketTask() -- 雙向(上傳、下載)資料交換到目前為止,我們都只用到 dataTask(),不管是下載圖片或是JSON資料,dataTask() 都足以勝任。那什麼情況下才需要用 downloadTask() 或其他工作呢?
本單元就先試用 downloadTask() 來下載大圖,看看與 dataTask() 有何不同。
芝加哥藝術博物館每個作品都提供高解析度的大圖,若要下載大圖,非常簡單,只要利用以下格式的網址:
https://www.artic.edu/iiif/2/cf50f037-5fb2-e197-0e56-3ae701edb3e2/full/max/0/default.jpg
對我們而言,只要知道作品的圖片編號(image_id),填入上述網址,就可抓到作品的大圖。
3-5a 抓大圖(downloadTask)
操作步驟如下:
- 選取畫面左方的「+」號新增電子書頁面。
- 將新增的電子書面頁命名為「(21)網路程式基礎-5整合篇」。
- 在「Main」模組中撰寫程式:
// 3-5a 抓大圖(downloadTask) // Created by Philip, Heman, Jean 2021/10/13 // Revised by Jean 2024/12/28 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))
3-5b 拖曳手勢(DragGesture)
// 3-5b 拖曳手勢(DragGesture) // Created by Heman, 2021/10/17 import PlaygroundSupport import SwiftUI 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() } var 拖曳: some Gesture { DragGesture(minimumDistance: 0.0, coordinateSpace: .local) .onChanged { 拖曳參數 in print("[onChanged]目前位置:", 拖曳參數.location) // print("[onChanged]目前位移 上次位移", 拖曳參數.translation, 上次位移) 位移.height = 上次位移.height + 拖曳參數.translation.height 位移.width = 上次位移.width + 拖曳參數.translation.width } .onEnded { 拖曳參數 in print("[onEnded]位移:", 拖曳參數.translation) if abs(拖曳參數.translation.height) < 3 && abs(拖曳參數.translation.width) < 3 { 縮放.toggle() 位移 = CGSize.zero 上次位移 = CGSize.zero } 上次位移.height += 拖曳參數.translation.height 上次位移.width += 拖曳參數.translation.width } } var body: some View { if 下載圖片 == nil { ProgressView() .onAppear { 下載() } } else { Image(uiImage: 下載圖片!) .resizable() .aspectRatio(contentMode: .fit) .scaleEffect(縮放 ? 5.0 : 1.0) .offset(位移) .gesture(拖曳) } } } //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))
- 程式執行結果,如下圖。
在此範例中,使用 downloadTask() 與原先的 dataTask() 不同的地方,只是在於「匿名函式」有一個參數不一樣。
原先 dataTask() 匿名函式的三個參數,分別為:
回傳資料(data) -- 回傳資料(如圖片或JSON),存放在記憶體中
回應碼(response) -- 回應的標頭(headers)
錯誤碼(error) -- 未獲得回應時的錯誤原因而 downloadTask() 匿名函式的三個參數,分別為:
暫存檔(fileURL) -- 回傳的資料,存放在硬碟(暫存檔)中
回應碼(response) -- 回應的標頭(headers)
錯誤碼(error) -- 未獲得回應時的錯誤原因因為 downloadTask() 會將回傳資料先存到暫存檔,所以我們仿照第2單元第7課從檔案讀入JSON資料的方式,來讀取暫存檔案,程式碼如下:
do { let 回傳資料 = try Data(contentsOf: 暫存檔!) if let 圖檔 = UIImage(data: 回傳資料) { print(回應碼 ?? "No response") 下載圖片 = 圖檔 } else { print(錯誤碼 ?? "No error") } } catch { print("Data I/O error") }
主要就是由 Data(contentsOf: 暫存檔!) 讀入檔案內容,再用 UIImage(data: 回傳資料) 轉成圖片。
這樣看起來,downloadTask() 跟 dataTask() 功能似乎沒什麼差別啊,為什麼要用 downloadTask() 呢?有以下幾個原因:
1. 當檔案較大時,downloadTask() 因為有暫存檔,萬一傳輸中斷,可以在網路恢復後繼續傳檔。
2. downloadTask() 下載的檔案大小幾乎沒有限制
3. 下載途中,可以控制暫停(suspend)、繼續(resume)或放棄(cancel)
4. 必要時,可以設定下載工作在「背景」執行,不影響畫面操作
5. 使用者「另存檔案」比較方便所以,什麼時候該用 dataTask(),什麼時候該用 downloadTask() 呢?這應該從「使用者體驗」的角度來看,如果資料量不大,下載時間只要幾秒鐘,就可用 dataTask(),不會讓使用者枯等,否則的話,若下載可能超過5秒鐘,則應改用 downloadTask()。
【註解】
1. IIIF 各欄位詳細說明可參考原始文件 [https://iiif.io/api/image/3.0/#41-region](https://iiif.io/api/image/3.0/#41-region)
2. 螢幕的「解析度」有兩種意義,第一種代表螢幕的畫素(pixel, 或稱像素)多寡,例如 Full HD 代表 1920x1080,也就寬1920畫素,高1080畫素,兩數相乘等於2,073,600,大約200萬畫素。而所謂 8K 螢幕,是指寬7680畫素,高4320畫素,相乘等於33,177,600,約3300萬畫素。
3. 「解析度」的第二種意義,是指畫素密度,通常用每英吋有多少畫素(dpi或ppi)來表示,例如 Apple 手機螢幕的解析度至少都 326dpi 以上,已超過眼睛所能辨識的最大密度。
4. 圖片為11世紀印度濕婆神「宇宙之舞」銅像(Shiva as Lord of the Dance),濕婆是印度教三大主神中,力量最強大的,主導宇宙的滅亡與再生,有眾多化身。圖片來源及說明可參考: [https://www.artic.edu/artworks/24548/shiva-as-lord-of-the-dance-nataraja](https://www.artic.edu/artworks/24548/shiva-as-lord-of-the-dance-nataraja)
5. 這張高解析度圖片尺寸為3480x3900,約1357萬畫素,檔案大小2.6MB,下載時間不長,用 dataTask() 或 downloadTask() 幾乎沒差別。芝加哥藝術博物館
在第2單元曾經提過非常重要的「視圖階層」觀念,SwiftUI每個視圖在螢幕的大小位置,是由上一層的「父視圖」所決定的,下圖的階層關係圖,將近10層,是不是有點誇張?這樣會不會影響執行的效能?想一想,有沒有辦法縮短?
- 在「Main」模組中撰寫程式:
// 3-5c 整合(芝加哥藝術博物館) // Created by Philip, Heman, Jean 2021/10/13 // Revised by Jean 2024/12/28 import PlaygroundSupport import SwiftUI // Refer to https://api.artic.edu/api/v1/artworks/search?q=von+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() } var 拖曳: some Gesture { DragGesture(minimumDistance: 0.0, coordinateSpace: .local) .onChanged { 拖曳參數 in print("[onChanged]目前位置:", 拖曳參數.location) // print("[onChanged]目前位移 上次位移", 拖曳參數.translation, 上次位移) 位移.height = 上次位移.height + 拖曳參數.translation.height 位移.width = 上次位移.width + 拖曳參數.translation.width } .onEnded { 拖曳參數 in print("[onEnded]位移:", 拖曳參數.translation) if abs(拖曳參數.translation.height) < 3 && abs(拖曳參數.translation.width) < 3 { 縮放.toggle() 位移 = CGSize.zero 上次位移 = CGSize.zero } 上次位移.height += 拖曳參數.translation.height 上次位移.width += 拖曳參數.translation.width } } var body: some View { if 下載圖片 == nil { ProgressView() .onAppear { 下載() } } else { Image(uiImage: 下載圖片!) .resizable() .aspectRatio(contentMode: .fit) .scaleEffect(縮放 ? 5.0 : 1.0) .offset(位移) .gesture(拖曳) } } } 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=von+gogh&limit=100" // 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=8" 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 目前作品.image_id = 單項作品!.image_id 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() } let 單欄 = [GridItem(.flexible()), GridItem(.flexible())] var body: some View { VStack(spacing: 0) { Text("Art Institute of Chicago") .font(.title) .bold() ZStack { Rectangle() .foregroundColor(.orange) .frame(height: 50) HStack { Image(systemName: "magnifyingglass") .font(.title2) TextField("Search for artworks", text: $inputText) { 搜尋字串 = inputText 作品列表 = nil 作品圖集 = [] } .font(.title2) .foregroundColor(.black) .background(Color.white) } .padding() } if 作品列表 == nil { ProgressView() .onAppear { 更新作品列表(搜尋字串) } } else { NavigationView { ScrollView { LazyVGrid(columns: 單欄) { ForEach(作品圖集) { 作品 in NavigationLink(destination: 抓大圖(作品.image_id)) { VStack { 抓圖(作品.圖檔網址!) HStack { Text(作品.作品名稱) .font(.body) .bold() Spacer() Text(作品.作者) .font(.callout) } } } } } } .navigationTitle(搜尋字串) Text("⬅︎左方導覽頁面選擇作品") } } } } } PlaygroundPage.current.setLiveView(芝加哥藝術博物館())
- 程式執行結果,如下圖。