精通 SwiftUI - iOS 17 版

第 36 章
利用 AsyncImage 非同步加載和顯示圖像

在 WWDC 2021 中,Apple 公布了 SwiftUI 框架的大量新功能,使開發者的開發 App 變得更得心應手。 AsyncImage 絕對是 iOS 15 其中一個最令人期待的功能。如果你的App需要從遠程伺服器加載和顯示圖像,這個新視圖應該可以讓你不必編寫自己的程式碼來處理非同步 (asynchronous) 下載。

AsyncImage 是用於非同步加載和顯示遠程圖像的內置視圖。 你只需要告訴它圖像的 URL, AsyncImage 就自動抓取遠程圖像並將其顯示在屏幕上。

在本章中,我將向你講解如何在 SwiftUI 項目中使用 AsyncImage

AsyncImage 的基本用法

使用 AsyncImage 的最簡單方法是像這樣指定圖像的 URL:

AsyncImage(url: URL(string: imageURL))

AsyncImage 就會自動連接到所指定的 URL 以非同步形式從遠程伺服器加載圖像。 當圖像尚未能夠顯示時,它還會自動將佔位符呈現為灰色。 完全下載後,AsyncImage 就會以其固有大小顯示圖像。

圖 36.1. 使用 AsyncImage
圖 36.1. 使用 AsyncImage

假設你已經在 Xcode 中建立了一個新的 SwiftUI 項目,你可以像這樣更改 ContentView 中的程式碼來試一試:

struct ContentView: View {
    let imageURL = "https://link.appcoda.com/testimage"

    var body: some View {
        AsyncImage(url: URL(string: imageURL))
    }
}

更改後,你應該會看到一個灰色的佔位符。數秒後,就會顯示下載的圖像。

如果要使圖像變小或變大,可以將縮放值傳給 scale 參數,如下所示:

AsyncImage(url: URL(string: imageURL), scale: 2.0)

要縮小圖像,你就指定一個大於 1.0 的值。 相反,小於 1 的值就會把圖像變大。

圖 36.2. 縮小圖像
圖 36.2. 縮小圖像

客製化圖像尺寸和佔位符

AsyncImage 也提供了另一個構造函數 (constructor),讓開發者可以進一步客製化圖像:

init<I, P>(url: URL?, scale: CGFloat, content: (Image) -> I, placeholder: () -> P)

我們可以使用上面的 init 初始化 AsyncImage,來調整及縮放下載好的圖像。更重要的是,我們可以實作自己的佔位符。看看以下的範例程式碼片段:

AsyncImage(url: URL(string: imageURL)) { image in
    image
        .resizable()
        .scaledToFill()
} placeholder: {
    Color.purple.opacity(0.1)
}
.frame(width: 300, height: 500)
.cornerRadius(20)

在上面的程式碼中,AsyncImage 提供了下載好的圖像。然後,我們應用 resizable()scaledToFill() 修飾符來調整圖像尺寸,並把 AsyncImage 視圖的尺寸限制為 300×500 points。

placeholder 參數讓我們可以創建自己的佔位符,來取代預設的佔位符。在以下範例中,我們把佔位符設置為淺紫色。

圖 36.3. 自訂佔位符
圖 36.3. 自訂佔位符

處理非同步操作的不同階段 (Phase)

如果你需要更精準操作非同步下載,AsyncImage 視圖提供了另一個init函數:

init(url: URL?, scale: CGFloat, transaction: Transaction, content: (AsyncImagePhase) -> Content

AsyncImagePhase 是一個列舉 (enum),用於跟踪下載操作的當前階段。 你可以針對每個階段提供詳細的實作,包括emptyfailuresuccess

看看以下範例程式碼片段:

AsyncImage(url: URL(string: imageURL)) { phase in
    switch phase {
    case .empty:
        Color.purple.opacity(0.1)
    case .success(let image):
        image
            .resizable()
            .scaledToFill()
    case .failure(_):
        Image(systemName: "exclamationmark.icloud")
            .resizable()
            .scaledToFit()
    @unknown default:
        Image(systemName: "exclamationmark.icloud")
    }
}
.frame(width: 300, height: 300)
.cornerRadius(20)

Empty 的情況下,表示圖像未加載,於是我們會顯示一個佔位符。在 success 的情況下,我們就會應用幾個修飾符,並將圖像顯示在螢幕上。在 failure 的情況下,我們就可以在出現錯誤時提供備用視圖 (alternate view)。在上面的程式碼中,我們就這樣顯示了一個系統圖像。

圖 36.4. 當圖檔下載遇到問題時,App會顯示一個系統圖像
圖 36.4. 當圖檔下載遇到問題時,App會顯示一個系統圖像

利用 Transaction 來添加動畫

同一個 init 可以讓我們在階段更改時指定可選 transaction。例如,以下程式碼片段在 transaction 參數中指定使用 spring 類型的動畫:

AsyncImage(url: URL(string: imageURL), transaction: Transaction(animation: .spring())) { phase in
    switch phase {
    case .empty:
        Color.purple.opacity(0.1)

    case .success(let image):
        image
            .resizable()
            .scaledToFill()

    case .failure(_):
        Image(systemName: "exclamationmark.icloud")
            .resizable()
            .scaledToFit()

    @unknown default:
        Image(systemName: "exclamationmark.icloud")
    }
}
.frame(width: 300, height: 500)
.cornerRadius(20)

如此一來,在下載圖像後,我們就會看到淡入 (fade-in) 動畫。我們無法在預覽版面中測試程式碼,請在模擬器中測試程式碼以查看動畫。

你也可以把 transition 修飾符附加到 image 視圖:

case .success(let image):
    image
        .resizable()
        .scaledToFill()
        .transition(.slide)

這樣在顯示結果圖像時,就會看到滑入 (slide-in) 動畫。

總結

在本章中,我們介紹了一個非常好用的「AsyncImage」視圖。有了這個新功能,下載和顯示遠程圖像(Remote Image)時都變得非常容易。 你只需要指定圖像的 URL,AsyncImage視圖就會為你完成所有繁重的工作。

在本章所準備的範例檔中,有最後完整的 Xcode 專案,可供你下載參考:

https://www.appcoda.com/resources/swiftui5/SwiftUIAsyncImage.zip