網路基礎-抓圖篇
-
3-6a 讀取網路圖片(AsyncImage)
Swift Playgrounds 4.0 增加了一個 AsyncImage 物件,讓我們寫程式下載網路圖片變得更容易。
操作步驟如下:
- 選取畫面左方的「+」號新增電子書頁面。
- 將新增的電子書面頁命名為「(22)網路程式基礎-6抓圖篇」。
- 在「Main」模組中撰寫程式:
// 3-6a 讀取網路圖片(AsyncImage) // Created by Philip, Heman, Jean 2021/12/16 // Revised by Jean 2024/12/28 import PlaygroundSupport import SwiftUI let 網址 = "https://picsum.photos/720/1280" struct 抓圖: View { var body: some View { AsyncImage(url: URL(string: 網址)) { 下載圖片 in 下載圖片 .resizable() .scaledToFit() } placeholder: { ProgressView() } } } PlaygroundPage.current.setLiveView(抓圖())
- 程式執行結果,如下圖。
AsyncImage 也是一個 View 物件類型,跟其他 SwiftUI 的 View 物件語法類似,所以用起來很方便。物件的初始化需要一個 URL 物件實例當作參數,下載取得的圖片會傳入後面接的匿名函式,在取得下載圖片之前,可指定另一個 View 當作緩衝(在此為 ProgressView()),這個緩衝的參數稱為 placeholder,就是臨時用來佔位置的東西(圖片或視圖)。=
3-6b AsyncImage phase
改用 AsyncImage 之後,同樣會有並行特性,以下範例我們連續用8次AsyncImage抓8張網路貓狗圖片,觀察它們顯示的順序,就可看到程式背後的並行特性。
- 在「Main」模組中撰寫程式:
// 3-6b AsyncImage phase // Created by Philip, Heman, Jean 2021/12/18 // Revised by Jean 2024/12/28 import PlaygroundSupport import SwiftUI struct 抓圖: View { var 網址: String var body: some View { AsyncImage(url: URL(string: 網址)) { 狀態 in if let 下載圖片 = 狀態.image { 下載圖片 .resizable() .scaledToFit() } else if 狀態.error != nil { Image(systemName: "xmark.icloud.fill") .scaleEffect(2) .foregroundColor(.red) } else { ProgressView() } } .frame(width: 200, height: 200) } } let 阿貓 = "https://thecatapi.com/api/images/get?format=src&type=jpg" let 阿狗 = "https://thedogapi.com/api/images/get?format=src&type=jpg" struct 網路上的阿貓阿狗: View { var body: some View { HStack { VStack { 抓圖(網址: 阿貓) 抓圖(網址: 阿貓) 抓圖(網址: "https://thecatapi.com/") 抓圖(網址: 阿貓) } VStack { 抓圖(網址: 阿狗) 抓圖(網址: "https://thedogapi.com/") 抓圖(網址: 阿狗) 抓圖(網址: 阿狗) } } } } PlaygroundPage.current.setLiveView(網路上的阿貓阿狗())
- 程式執行結果,如下圖。
此範例顯示視圖「網路上的阿貓阿狗」,連續使用8次「抓圖」,分兩列 4 x 2 顯示。
第一列是連到提供貓貓圖片的API網站,另一列是狗狗網站。其中兩張圖片網址故意寫錯,會顯示紅色的系統圖示。
程式中「抓圖」的功能與上一節相同,但是寫法有兩個差異:一是增加一個物件屬性「網址」,讓我們可以傳不同網址進去;二是使用 AsyncImage 另外一種語法,詳述如下。
在上一單元註解曾提過,Swift 同一個物件通常允許多種用法,用法之間以參數的差異來做區別。
本節 AsyncImage 用法與上一單元不同的地方,在於少掉 placeholder 參數,這時候帶入匿名函式的參數類型,會與上一單元有所不同。
AsyncImage(url: URL(string: 網址)) { 下載圖片 in 下載圖片 .resizable() .scaledToFit() } placeholder: { ProgressView() }
在上一單元的 AsyncImage 有 placeholder 參數,這時候傳入匿名函式的參數是Image類型的「下載圖片」,在取得圖片之前,則顯示 placeholder 裡面的 ProgressView()。
3-6a 這段程式碼有個缺失,就是當「網址」錯誤,或是網路斷線等原因,無法正確取得圖片時,畫面會一直顯示ProgressView(),也就是不停地轉圈,讓使用者空等。
過去我們用 URLSession.shared.dataTask() 抓圖時,對錯誤狀況必須在程式中一一判別加以應對,對程式設計師來說,「例外處理」是一件重要但很費心的工作。
所幸 AsyncImage 另一種用法可以協助處理例外情況(注意沒有 placeholder參數):
範例3-6b
AsyncImage(url: URL(string: 網址)) { 狀態 in if let 下載圖片 = 狀態.image { 下載圖片 .resizable() .scaledToFit() } else if 狀態.error != nil { Image(systemName: "xmark.icloud.fill") .scaleEffect(2) .foregroundColor(.red) } else { ProgressView() } }
這時候,傳入匿名函式的參數不是Image類型,而是Image類型之外再包一層屬性,官方稱為AsyncImagePhase類型,所以文件中,傳入匿名函式的參數名稱取為 phase (階段),但在本課取名為「狀態」。
參數「狀態(或 phase)」有三種列舉(enum)值:
1. empty: 尚未取得 Response 時
2. success: 成功取得圖片(包含Image物件)
3. failure: 連線失敗或未取得圖片(包含Error物件)所以當「狀態 == .success」,則「狀態.image」就是獲取的圖片,如果「狀態 == .error」則「狀態.error」就包含錯誤訊息(Error 物件)。
因此,上述3-6b程式碼直接測試「狀態.image」是否有值,如果有,就指定給「下載圖片」並顯示出來,如果沒有值(狀態.image == nil),就再看看「狀態.error」有沒有值,若有就代表連線出錯,我們就顯示一個紅色的系統圖示"xmark.icloud.fill",若沒有,就表示「狀態 == .empty」,還未收到 Response,就顯示 ProgressView()。
當「狀態」的值有變化時,例如從 .empty 變成 .success,或從 .empty 變成 .failure,AsyncImage 會重新計算,畫面就會從 ProgressView() 變成 Image()。
所以這樣的語法一次解決了連線過程的三種狀況,顯然比上一單元的語法更好用。
【註解】
1. 注意 AsyncImage 是 View 類型,但不是 Image 類型,在第二單元曾經提過,View 是大類別,下面包含了 Text, Image, VStack...等小類別,故 AsyncImage 和 Image 是不同的物件類型。
2. 所以 AsyncImage 物件可以用 View 修飾語,例如 .frame(),但不能直接用 Image 修飾語,例如 .resizable(), .scaleToFit()。
3. 當下載圖片成功之後,包在 AsyncImage 裡面的才是 Image 類型;Image 與 UIImage 兩者類型也不相同,之前用 URLSession.shared.dataTask() 下載的圖片資料可轉換為 UIImage 類型,屬於 UIKit 物件庫,而 Image 與 AsyncImage 都屬於 SwiftUI 物件庫。
4. 事實上,Image 類型是在 UIImage 外再包一層屬性,而 UIImage 則是在更基礎的 CGImage 外再包一層。
5. 本單元範例程式 AsyncImage 段落修改自官網文件,是 AsyncImage 物件的標準句型,值得背下來。