圖片輪播是個非常實用的動畫效果,在網站或App應用中經常看到,很適合拿來播放廣告或展示商品,每次只顯示一個畫面,停留一段時間後,自動滑入下一個畫面。為了要達到最好的顯示效果,通常圖片會盡量佔滿整個螢幕,文字或操作按鈕浮在圖片上層,類似抖音(TikTok)的做法。

聽起來很簡單,不過實際做起來卻不容易,關鍵之處在於下一張水平滑入時,與前一張一起移動的距離,恰好到螢幕寬度(若是垂直滑入,則是螢幕高度)。所以跳轉時必須計算螢幕的寬或高,慢慢滑入下一張圖片,以 SwiftUI 現有的排版視圖,包括 ScrollView, LazyVGrid, List…等都無法做到理想的圖片輪播。

本課我們嘗試利用畫布 Canvas 來製作圖片輪播的效果。

圖片內容從哪裡來呢?有兩種來源。在第2單元,我們是將圖片檔案導入 Swift Playgrounds,與App包在一起;在第3單元則是直接從網路上抓圖,為方便起見,本課利用第3單元學過的網路程式 URLSession 來抓圖。

IV-9a Canvas 圖片下載

我們還未曾在畫布Canvas 中用過網路程式,如果還記得第3單元課程,其實配合起來非常簡單,參考「表3-8 非同步+錯誤處理指令」,使用非同步的 URLSession.shared.data() 下載圖片,可以用 Task-await + do-try-catch 的句型,如下:

let 網址 = "<https://picsum.photos/1080/1920>"
Task {
    if let myURL = URL(string: 網址) {
        do {
            let (內容, 回應碼) = try await URLSession.shared.data(from: myURL)
            if let 轉圖 = UIImage(data: 內容) {
                ...
            }
        } catch {
            print("有錯誤發生")
        }
    }
}

這段程式碼會從「網址」隨機下載一張寬1080x高1920解析度的圖片資料,剛下載的「內容」稱為原始資料(raw data),須經過 UIImage() 轉換成圖片格式(如果是JSON資料,則透過傑森解碼器轉換),才可用 Image 視圖或畫布 Canvas 顯示出來。

上面句型是命令式語法,若要用在視圖中,只須將 Task 改成 .task 修飾語即可,.task 相當於 .onAppear 的非同步版本,因此會在視圖一出現時就執行。以下程式我們先讓畫布顯示一個空的「圖片集」,然後透過 .task 到網址下載圖片,等圖片加入「圖片集」,因為是狀態變數,會自動更新視圖,將圖片顯示出來。

完整程式碼如下:

// 4-9a Canvas 圖片下載
// Created by Heman, 2022/06/17
import PlaygroundSupport
import SwiftUI

let 網址 = "<https://picsum.photos/1080/1920>"
// 備用網址:
// let 網址 = "<https://source.unsplash.com/collection/1027750/1080x1920>"

struct 畫布: View {
    @State var 圖片集: [UIImage] = []
    
    var body: some View {
        Canvas { 圖層, 尺寸 in
            let 中心 = CGPoint(x: 尺寸.width/2, y: 尺寸.height/2)
            if 圖片集 == [] {
                圖層.draw(Text("等待圖片下載..."), at: 中心, anchor: .center)
            } else {
                let 照片 = 圖層.resolve(Image(uiImage: 圖片集.first!))
                let 寬度比 = 尺寸.width / 照片.size.width
                let 高度比 = 尺寸.height / 照片.size.height
                let 縮放比 = min(寬度比, 高度比)
                圖層.translateBy(x: 中心.x, y: 中心.y)
                print(尺寸, 圖層.transform)
                if 縮放比 < 1.0 {
                    圖層.scaleBy(x: 縮放比, y: 縮放比)
                    print(尺寸, 圖層.transform)
                }
                圖層.draw(照片, at: .zero)
            }
        } 
        .border(.red)
        .task {
            if let myURL = URL(string: 網址) {
                do {
                    let (內容, 回應碼) = try await URLSession.shared.data(from: myURL)
                    print(回應碼)
                    if let 轉圖 = UIImage(data: 內容) {
                        圖片集.append(轉圖)
                    }
                } catch {
                    print("無法下載圖片")
                }
            }
        }
    }
}

PlaygroundPage.current.setLiveView(畫布())

畫布在一開始(圖片集 == [])會顯示一段文字:"等待圖片下載...",這是使用圖層的 draw() 來顯示,參數 at: 指定畫布中的一個點座標,這個位置會對準整行文字的中央,也就是第3個參數 anchor: .center 的用途,anchor 是船錨或錨定的意思。

if 圖片集 == [] {
    圖層.draw(Text("等待圖片下載..."), at: 中心, anchor: .center)
}