精通 SwiftUI - iOS 17 版

第 9 章
基礎動畫與轉場

你曾在 Keynote 使用過瞬間移動動畫( magic move animation )嗎?藉由瞬間移動效果, 你可以輕鬆建立投影片間的平滑動畫( slick animation )。Keynote 會自動分析兩張投影片之間的物件,並自動渲染動畫。同樣,SwiftUI 也將瞬間移動動畫帶入應用程式開發中, 使用該框架的動畫是自動且神奇的。當你定義一個視圖的兩個狀態,SwiftUI 會找出其餘的狀態,接著以動畫呈現兩個狀態之間的變化。

SwiftUI 使你能夠為單個視圖的變化以及兩個視圖之間的轉場來設定動畫。SwiftUI 框架具有許多的內建動畫,可建立不同的效果。

隱式動畫與顯式動畫

SwiftUI 提供兩種動畫類型:隱式( implicit )與顯式( explicit )。這兩個方式可以讓你為視圖及視圖轉場設定動畫。為了實作隱式動畫,SwiftUI 框架提供一個名為 animation 的修飾器,你把這個修飾器加到要設定動畫的視圖上,並指定喜歡的動畫類型。或者,你可以定義動畫的持續時間與延遲時間,SwiftUI 會根據視圖的狀態變化來自動渲染動畫。

顯式動畫對你要顯示的動畫提供更具侷限性的控制,其並非將修飾器加到視圖,而是在 withAnimation() 區塊中,告訴 SwiftUI 若有什麼的狀態變化時,要繪製動畫。

仍是覺得有些困惑嗎?沒有關係,藉由幾個範例,你就會更有概念了。

隱式動畫

我們從隱式動畫來開始介紹,我建議你建立一個新專案來看動畫的實際效果。你可以任意為專案命名,而我將它命名為 SwiftUIAnimation

圖 9.1. 繪製按鈕的狀態變化
圖 9.1. 繪製按鈕的狀態變化

我們來看圖 9.1,這是一個簡單的可點擊視圖,由紅色圓形與心形所組成。當使用者點擊心形或圓形時,圓形的顏色會變成淡灰色,而心形則會變成紅色,同時心形圖示的大小也變得較大。因此,以下是其狀態變化:

  1. 圓的顏色會從紅色變成淡灰色。
  2. 心形圖示的顏色會從白色變成紅色。
  3. 心形圖示會比原來的大小大兩倍。

如果你使用SwiftUI 來實作可點擊的圓形,以下為程式內容,你可以建立一個 Xcode 的新專案,並將程式插入至 ContentView.swift 來測試看看:

struct ContentView: View {
    @State private var circleColorChanged = false
    @State private var heartColorChanged = false
    @State private var heartSizeChanged = false

    var body: some View {

        ZStack {
            Circle()
                .frame(width: 200, height: 200)
                .foregroundColor(circleColorChanged ? Color(.systemGray5) : .red)

            Image(systemName: "heart.fill")
                .foregroundColor(heartColorChanged ? .red : .white)
                .font(.system(size: 100))
                .scaleEffect(heartSizeChanged ? 1.0 : 0.5)
        }
        .onTapGesture {
            circleColorChanged.toggle()
            heartColorChanged.toggle()
            heartSizeChanged.toggle()
        }

    }
}

我們定義三個狀態變數來建立狀態模型,初始值皆設定為「false」。為了建立圓形與心形,我們使用 ZStack 來將心形疊在圓形上,如圖9.2 所示。SwiftUI 有個 onTapGesture 修飾器可以偵測,點擊手勢,你可以將它加在任何視圖,使其可點擊。在 onTapGesture 閉包中,我們切換狀態,以改變視圖的外觀。

圖 9.2. 實作圓形與心形視圖
圖 9.2. 實作圓形與心形視圖

在預覽畫布測試這個 App,則你點擊視圖時,圓形及心形圖示的顏色會改變,但這些變化不會顯示動畫。

要讓這些變化顯示動畫效果,你只需將 animation 修飾器加到 CircleImage 視圖:

Circle()
    .frame(width: 200, height: 200)
    .foregroundColor(circleColorChanged ? Color(.systemGray5) : .red)
    .animation(.default, value: circleColorChanged)

Image(systemName: "heart.fill")
    .foregroundColor(heartColorChanged ? .red : .white)
    .font(.system(size: 100))
    .scaleEffect(heartSizeChanged ? 1.0 : 0.5)
    .animation(.default, value: heartSizeChanged)

SwiftUI 會自動計算與渲染動畫,使視圖可以從一個狀態流暢轉換到另一個狀態。再次點擊心形,你會見到一個平滑動畫。

你不僅可以將 animation 修飾器應用到單一視圖中,還可應用於一組視圖。舉例而言, 你可以將 animation 修飾器加到 ZStack,將上列的程式碼重新撰寫如下:

ZStack {
    Circle()
        .frame(width: 200, height: 200)
        .foregroundColor(circleColorChanged ? Color(.systemGray5) : .red)

    Image(systemName: "heart.fill")
        .foregroundColor(heartColorChanged ? .red : .white)
        .font(.system(size: 100))
        .scaleEffect(heartSizeChanged ? 1.0 : 0.5)
}
.animation(.default, value: circleColorChanged)
.animation(.default, value: heartSizeChanged)
.onTapGesture {
    self.circleColorChanged.toggle()
    self.heartColorChanged.toggle()
    self.heartSizeChanged.toggle()
}

其工作原理是完全相同的,SwiftUI 尋找嵌入在 ZStack 中所有的狀態變化,並建立動畫。

在範例中,我們使用預設動畫。SwiftUI 提供許多內建動畫供你選擇,包括 lineareaseIneaseOuteaseInOutspring。「線性動畫」(linear animation )是以線性速度來呈現變化,而「緩動動畫」(easing animation )則是速度會做變化。詳細內容可以參考 www.easings.net 來了解每個 ease 函數的差異。

要使用其他的動畫,你只需要在 animation 修飾器中設定特定的動畫。例如:你想要使用 spring 動畫,則可以將 .default 更改如下:

.animation(.spring(response: 0.3, dampingFraction: 0.3, blendDuration: 0.3), value: circleColorChanged)

這會渲染一個基於彈簧特性的動畫,使得心形產生心跳的效果。你可以調整阻尼(damping )值與混合(blend )值,來達到不同的效果。

顯式動畫

以上是對視圖使用隱式動畫的方法。我們來看如何使用顯式動畫來達到相同的結果。如前所述,你需要將狀態變化包裹在 withAnimation 區塊內。要建立相同的動畫效果,你可撰寫下列程式碼:

ZStack {
    Circle()
        .frame(width: 200, height: 200)
        .foregroundColor(circleColorChanged ? Color(.systemGray5) : .red)

    Image(systemName: "heart.fill")
        .foregroundColor(heartColorChanged ? .red : .white)
        .font(.system(size: 100))
        .scaleEffect(heartSizeChanged ? 1.0 : 0.5)
}
.onTapGesture {
    withAnimation(.default) {
        self.circleColorChanged.toggle()
        self.heartColorChanged.toggle()
        self.heartSizeChanged.toggle()
    }
}

我們不再使用 animation 修飾器,而是使用 withAnimation 包裹在 onTapGesture 中。withAnimation 呼叫帶入一個動畫參數,這裡我們指定使用預設動畫。

當然,你可更新 withAnimation,以將動畫變更為彈簧動畫,如下所示:

withAnimation(.spring(response: 0.3, dampingFraction: 0.3, blendDuration: 0.3)) {
    self.circleColorChanged.toggle()
    self.heartColorChanged.toggle()
    self.heartSizeChanged.toggle()
}

使用顯式動畫,你可以輕鬆控制想加上動畫的狀態。舉例而言,如果你不想為心形圖示的大小變化設定動畫時,則可以從 withAnimation 排除該行程式碼,如下所示:

.onTapGesture {
    withAnimation(.spring(response: 0.3, dampingFraction: 0.3, blendDuration: 0.3)) {
        self.circleColorChanged.toggle()
        self.heartColorChanged.toggle()
    }

    self.heartSizeChanged.toggle()
}

在這個情況下,SwiftUI 將只對圓形與心形的顏色變化設定動畫,你不會再看到心形圖示的變大動畫效果。

你可能想知道我們是否可以使用隱式動畫來關閉縮放動畫。好的,你仍然可以做到, 可重新調整 .animation 的順序,以防止 SwiftUI 對特定狀態變化設定動畫。下列為達到相同效果的程式:

ZStack {
    Circle()
        .frame(width: 200, height: 200)
        .foregroundColor(circleColorChanged ? Color(.systemGray5) : .red)
        .animation(.spring(response: 0.3, dampingFraction: 0.3, blendDuration: 0.3), value: circleColorChanged)

    Image(systemName: "heart.fill")
        .foregroundColor(heartColorChanged ? .red : .white)
        .font(.system(size: 100))
        .animation(.spring(response: 0.3, dampingFraction: 0.3, blendDuration: 0.3), value: heartColorChanged)
        .scaleEffect(heartSizeChanged ? 1.0 : 0.5)
}
.onTapGesture {
    self.circleColorChanged.toggle()
    self.heartColorChanged.toggle()
    self.heartSizeChanged.toggle()
}

對於 Image 視圖,我們在 scaleEffect 之前置入 animation(nil) 修飾器,這會將動畫取消,scaleEffect 修飾器的狀態變化將不會設定動畫。

雖然你可以使用隱式動畫建立相同的動畫,但我認為在這種情況下,使用顯式動畫會更方便。

使用RotationEffect 建立下載指示器

SwiftUI 動畫的強大之處在於,你不需關心如何對視圖設定動畫,你只需要提供起始與結束狀態,接著 SwiftUI 會找出其餘的狀態。當你具備了這個觀念,即可以建立各種類型的動畫。

圖 9.3. 下載指示器範例
圖 9.3. 下載指示器範例

舉例而言,我們來建立一個簡單的下載指示器,你通常可以在一些真實世界的應用程式(如 Medium)中找到它。要建立一個如圖 9.3 所示的下載指示器,我們從開放式圓環(open ended circle )來開始,如下所示:

Circle()
    .trim(from: 0, to: 0.7)
    .stroke(Color.green, lineWidth: 5)
    .frame(width: 100, height: 100)

那麼,我們如何使圓環連續旋轉呢?我們可以利用 rotationEffectanimation 修飾器。訣竅就是使圓環 360 度連續旋轉。以下為程式碼:

struct ContentView: View {
    @State private var isLoading = false

    var body: some View {
        Circle()
            .trim(from: 0, to: 0.7)
            .stroke(Color.green, lineWidth: 5)
            .frame(width: 100, height: 100)
            .rotationEffect(Angle(degrees: isLoading ? 360 : 0))
            .animation(.default.repeatForever(autoreverses: false), value: isLoading)
            .onAppear() {
                isLoading = true
            }
    }
}

rotationEffect 修飾器帶入旋轉角度,在上列的程式碼中,我們有一個狀態變數來控制下載狀態。當它設定為true 時,這個旋轉角度設定為 360 度以旋轉圓環。在 animation 修飾器中,我們指定使用預設動畫,不過還有些不同,我們告訴 SwiftUI 要一次又一次重複相同的動畫,這就是建立下載動畫的訣竅。

*備註: 如果在預覽畫布中看不到動畫,請在模擬器中運行App再試一試。*

如果你想要更改動畫的速度,則可以使用線性動畫,並指定持續時間,如下所示:

.animation(.linear(duration: 5).repeatForever(autoreverses: false), value: isLoading)

持續時間越久,則動畫越慢。

onAppear 修飾器對你而言可能比較陌生,如果你對 UIKit 有所了解的話,這個修飾器和 viewDidAppear 非常相似,當視圖出現在畫面上時會自動呼叫。在該程式碼中,我們將下載狀態設定為true,以在視圖載入時啟動動畫。

當你使用此技術,就可以調整設計並開發各種版本的下載指示器。舉例而言,你可以將圓弧疊在圓環上,以建立精美的下載指示器,如圖 9.4 所示。

圖 9.4. 下載指示器範例
圖 9.4. 下載指示器範例

程式碼片段如下所示:

struct ContentView: View {

    @State private var isLoading = false

    var body: some View {
        ZStack {

            Circle()
                .stroke(Color(.systemGray5), lineWidth: 14)
                .frame(width: 100, height: 100)

            Circle()
                .trim(from: 0, to: 0.2)
                .stroke(Color.green, lineWidth: 7)
                .frame(width: 100, height: 100)
                .rotationEffect(Angle(degrees: isLoading ? 360 : 0))
                .animation(.linear(duration: 1).repeatForever(autoreverses: false), value: isLoading)
                .onAppear() {
                    self.isLoading = true
            }
        }
    }
}

下載指示器不需要為圓形,你也可以使用 RectangleRoundedRectangle 來建立該指示器。不過,無需更改旋轉角度,你可以修改位移值(offset value)來建立如圖 9.5 所示的動畫。

圖 9.5. 下載指示器的另一個範例
圖 9.5. 下載指示器的另一個範例

為了建立動畫,我們將兩個圓角矩形重疊在一起。上面的矩形比下面的矩形短得多, 當開始載入時,我們將位移值從「-110」更新為「110」。

struct ContentView: View {

    @State private var isLoading = false

    var body: some View {
        ZStack {

            Text("Loading")
                .font(.system(.body, design: .rounded))
                .bold()
                .offset(x: 0, y: -25)

            RoundedRectangle(cornerRadius: 3)
                .stroke(Color(.systemGray5), lineWidth: 3)
                .frame(width: 250, height: 3)

            RoundedRectangle(cornerRadius: 3)
                .stroke(Color.green, lineWidth: 3)
                .frame(width: 30, height: 3)
                .offset(x: isLoading ? 110 : -110, y: 0)
                .animation(.linear(duration: 1).repeatForever(autoreverses: false), value: isLoading)
        }
        .onAppear() {
            self.isLoading = true
        }
    }
}

這會讓綠色矩形沿著線條移動。而且,當你一次又一次重複相同的動畫,它將變成一個載入動畫,圖 9.6 說明了位移值。

圖 9.6. 下載指示器的另一個範例
圖 9.6. 下載指示器的另一個範例

建立進度指示器

下載指示器向使用者提供一些回饋,其表示 App 正在處理某些事情。不過,它並沒有顯示實際進度,如果你需要為使用者提供關於任務進度的更多資訊,則可能需要建立一個如圖 9.7 所示的進度指示器。

圖 9.7. 進度指示器
圖 9.7. 進度指示器

建立進度指示器的方式與下載指示器非常相似。不過,你需要使用狀態變數來追蹤進度。以下是建立進度指示器的程式碼片段:

struct ContentView: View {
    @State private var progress: CGFloat = 0.0

    var body: some View {

        ZStack {    
            Text("\(Int(progress * 100))%")
                .font(.system(.title, design: .rounded))
                .bold()

            Circle()
                .stroke(Color(.systemGray5), lineWidth: 10)
                .frame(width: 150, height: 150)

            Circle()
                .trim(from: 0, to: progress)
                .stroke(Color.green, lineWidth: 10)
                .frame(width: 150, height: 150)
                .rotationEffect(Angle(degrees: -90))
        }
        .onAppear() {
            Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { timer in
                self.progress += 0.05
                print(self.progress)
                if self.progress >= 1.0 {
                    timer.invalidate()
                }
            }
        }
    }
}

這裡的狀態變數不是使用布林值,而是我們使用浮點數來儲存狀態。為了顯示進度, 我們以進度值來設定 trim 修飾器。在真實世界的應用程式中,你可以更新 progress 的值來顯示操作的實際進度。為了示範,我們只啟用一個計時器,其每半秒更新一次。

延遲動畫

SwiftUI 框架不只讓你可以控制動畫的持續時間,你可以透過 delay 函數來延遲動畫, 如下所示:

Animation.default.delay(1.0)

這會將動畫延遲1 秒後開始。delay 函數也適用其他動畫。

透過混合搭配持續時間值與延遲時間值,你可以實作出一些有趣的動畫,如圖 9.8 所示的圓點下載指示器。

圖 9.8. 圓點下載指示器r
圖 9.8. 圓點下載指示器r

這個指示器由五個點組成,每個點皆有放大縮小動畫,不過各有不同的延遲時間。我們來看程式碼該如何實作:

struct ContentView: View {
    @State private var isLoading = false

    var body: some View {
        HStack {
            ForEach(0...4, id: \.self) { index in
                Circle()
                    .frame(width: 10, height: 10)
                    .foregroundColor(.green)
                    .scaleEffect(self.isLoading ? 0 : 1)
                    .animation(.linear(duration: 0.6).repeatForever().delay(0.2 * Double(index)), value: isLoading)
            }
        }
        .onAppear() {
            self.isLoading = true
        }
    }
}

我們先使用一個 HStack 來水平佈局這些圓形,由於這五個圓形(也就是點)皆有相同的大小與顏色,因此我們使用 ForEach 來建立這些圓形。scaleEffect 修飾器是用來縮放圓形的大小,預設是設定為「1」,也就是原來的大小。當開始載入時,該值會更新為「0」, 這將會縮小此點。

用於渲染動畫的程式碼看起來有些複雜,我們來分拆它並逐步研究:

Animation.linear(duration: 0.6).repeatForever().delay(0.2 * Double(index))

第一個部分建立一個持續時間為 0.6 秒的線性動畫,該動畫會重複執行,因此我們呼叫 repeatForever 函數。

如果你沒有呼叫 delay 函數來執行這個動畫,則所有的點會同時縮放。但是,這不是我們想要的結果,每個點應獨立調整大小,而不是全部同時放大/ 縮小,這也是為何我們呼叫 delay 函數的原因,並對每個點使用不同的延遲時間值。

你可以更改持續時間值與延遲時間值來調整動畫。

將矩形變形為圓形

有時,你可能需要將一個形狀(例如:矩形)流暢地變形為另一個形狀(例如:圓形)。這該如何實作呢?使用內建的形狀與動畫,你可以輕鬆建立如圖 9.9 所示的變形。

圖 9.9. 將矩形變形為圓形
圖 9.9. 將矩形變形為圓形

將矩形變形為圓形的技巧是使用 RoundedRectangle 形狀,並為圓角半徑的變化設定動畫。假設矩形的寬度與高度相同,當圓角半徑設定為寬度的一半時,它會變為圓形。以下是變形按鈕的實作:

struct ContentView: View {
    @State private var recordBegin = false
    @State private var recording = false

    var body: some View {
        ZStack {

            RoundedRectangle(cornerRadius: recordBegin ? 30 : 5)
                .frame(width: recordBegin ? 60 : 250, height: 60)
                .foregroundColor(recordBegin ? .red : .green)
                .overlay(
                    Image(systemName: "mic.fill")
                        .font(.system(.title))
                        .foregroundColor(.white)
                        .scaleEffect(recording ? 0.7 : 1)
                )

            RoundedRectangle(cornerRadius: recordBegin ? 35 : 10)
                .trim(from: 0, to: recordBegin ? 0.0001 : 1)
                .stroke(lineWidth: 5)
                .frame(width: recordBegin ? 70 : 260, height: 70)
                .foregroundColor(.green)

        }
        .onTapGesture {
            withAnimation(Animation.spring()) {
                self.recordBegin.toggle()
            }

            withAnimation(Animation.spring().repeatForever().delay(0.5)) {
                self.recording.toggle()
            }
        }
    }
}

這裡有兩個狀態變數:recordBeginrecording,其是控制兩個單獨的動畫。第一個變數控制按鈕的變形,如前所述,我們使用圓角半徑來實現這個變形。矩形的寬度原先是設定為「250 點」,當使用者點擊矩形來觸發變形時,這個框架的寬度會變為「60 點」。隨著改變,圓角半徑變成「30 點」,也就是寬度的一半。

這就是我們將矩形變形為圓形的方法,而且SwiftUI 會自動渲染此變形的動畫。

另一方面,recording 狀態變數處理了麥克風圖片的縮放。當它為錄音狀態時,我們將縮放比例從「1」更改為「0.7」,藉由重複執行相同的動畫,即可建立放大及縮小的動畫。

請注意,以上的程式碼使用顯式方法來對視圖設定動畫。這不是強制性的,依照自己的喜好,你也可以使用隱式動畫方法來獲得相同的結果。

了解轉場

到目前為止,我們已經討論了對視圖層次(view hierarchy )中已存在的視圖設定動畫。我們建立動畫來放大和縮小視圖,或者對視圖大小設定動畫。

SwiftUI 讓開發者不只是做出前述的動畫,你可以定義如何從視圖層次中插入或移除視圖,而在 SwiftUI 中,這就是所謂的「轉場」( transition )。框架預設是使用淡入( fade in )與淡出( fade out )轉場。不過它內建了幾個現成的轉場效果,如滑動( slide )、移動( move)、不透明度(opacity )等。當然,你可以開發自己的轉場效果,也可以簡單的混合搭配各種類型的轉場,以建立所需的轉場效果。

圖 9.10. 使用 SwiftUI 建立的轉場範例
圖 9.10. 使用 SwiftUI 建立的轉場範例

建立簡單的轉場

我們來看一個簡單的範例,以更加了解轉場的含義以及動畫如何運作。建立一個名為 SwiftUITransition 的新專案,並更新 ContentView 如下:

struct ContentView: View {

    var body: some View {
        VStack {
            RoundedRectangle(cornerRadius: 10)
                .frame(width: 300, height: 300)
                .foregroundColor(.green)
                .overlay(
                    Text("Show details")
                        .font(.system(.largeTitle, design: .rounded))
                        .bold()
                        .foregroundColor(.white)

            )

            RoundedRectangle(cornerRadius: 10)
                .frame(width: 300, height: 300)
                .foregroundColor(.purple)
                .overlay(
                    Text("Well, here is the details")
                        .font(.system(.largeTitle, design: .rounded))
                        .bold()
                        .foregroundColor(.white)
                )
        }
    }
}

在上列的程式碼中,我們使用 VStack 垂直佈局兩個矩形。而我要做的是讓這個堆疊可以點擊。首先,要隱藏紫色矩形,只有當使用者點擊綠色矩形(也就是Show details )時才會顯示。

圖 9.11. 垂直佈局兩個矩形
圖 9.11. 垂直佈局兩個矩形

為此,我們需要宣告一個狀態變數來決定是否顯示紫色矩形。將下列這行程式碼插入 ContentView 中:

@State private var show = false

接下來,我們將紫色矩形包裹在 if 敘述中,如下所示:

if show {
    RoundedRectangle(cornerRadius: 10)
        .frame(width: 300, height: 300)
        .foregroundColor(.purple)
        .overlay(
            Text("Well, here is the details")
                .font(.system(.largeTitle, design: .rounded))
                .bold()
                .foregroundColor(.white)
        )
}

對於 VStack,我們加入 onTapGesture 函數來偵測點擊,並為狀態變化建立動畫。請注意,該轉場效果與動畫要有關聯,否則它無法自行運作。

.onTapGesture {
    withAnimation(Animation.spring()) {
        self.show.toggle()
    }
}

當使用者點擊堆疊時,我們切換為 show 變數來顯示紫色矩形。如果你在模擬器或預覽畫布中執行這個 App,則應該只會看到綠色矩形,如圖 9.12 所示。點擊它會顯示紫色矩形,而你應可看到一個平滑的淡入與淡出的轉場效果。

圖 9.12. 預設的轉場效果
圖 9.12. 預設的轉場效果

如前所述,如果你沒有指定想使用的轉場效果,SwiftUI 將渲染淡入淡出轉場。要使用其他的轉場效果,則在紫色矩形中加入 transition 修飾器,如下所示:

if show {
    RoundedRectangle(cornerRadius: 10)
        .frame(width: 300, height: 300)
        .foregroundColor(.purple)
        .overlay(
            Text("Well, here is the details")
                .font(.system(.largeTitle, design: .rounded))
                .bold()
                .foregroundColor(.white)
        )
        .transition(.scale(scale: 0, anchor: .bottom))
}

這個 transition 修飾器會帶入一個 AnyTransition 型別的參數。這裡我們使用scale 轉場, 錨點(anchor )設定為 .bottom,這就是你修改轉場效果所需要做的事情。在模擬器中執行該 App,並看一下結果為何,當 App 顯示紫色矩形時,你應該會看到一個彈出動畫。我建議使用內建的模擬器來測試動畫,而不採用 App 預覽的方式,因為預覽畫布可能無法正確的渲染轉場動作。

圖 9.13. 縮放轉場效果
圖 9.13. 縮放轉場效果

除了 .scale 之外,SwiftUI 框架還有多個內建的轉場效果,包括 .opaque.offset.move.slide。試著以位移轉場(offset transition)取代縮放轉場(scale transition),如下所示:

.transition(.offset(x: -600, y: 0))

如此,當紫色矩形插入 VStack 時,它會從左側滑入。

混合式轉場

你可以呼叫 combined(with:) 方法來將兩個或者更多個轉場效果結合在一起,以建立更流暢的轉場效果。舉例而言,如果要結合位移與縮放動畫,則可以撰寫程式碼如下:

.transition(AnyTransition.offset(x: -600, y: 0).combined(with: .scale))

如果需要混合三個轉場效果的話,則可以參考以下這行的範例程式碼:

.transition(AnyTransition.offset(x: -600, y: 0).combined(with: .scale).combined(with: .opacity))

在某些情況下,如果你需要定義一個可以重複利用的動畫,你可以在 AnyTransition 定義一個擴展(extension ),如下所示:

extension AnyTransition {
    static var offsetScaleOpacity: AnyTransition {
        AnyTransition.offset(x: -600, y: 0).combined(with: .scale).combined(with: .opacity)
    }
}

接著,你可以在 transition 修飾器中使用 offsetScaleOpacity 動畫:

.transition(.offsetScaleOpacity)

執行這個App,並再次測試轉場效果,看起來很棒吧?

圖 9.14. 結合縮放、位移與不透明度的轉場效果
圖 9.14. 結合縮放、位移與不透明度的轉場效果

不對稱轉場

我們剛才討論的轉場皆是對稱性的,也就是視圖的插入與移除是使用相同的轉場效果。舉例而言,如果將縮放轉場運用於視圖,則 SwiftUI 在它插入視圖層次時會放大視圖,而移除它時,該框架會將其縮回原來大小。

那麼,若你想在插入視圖時使用縮放轉場以及移除視圖時使用位移轉場呢?這在 SwiftUI 中,即所謂的「不對稱轉場」( Assymetric Transitions )。要使用這種轉場效果非常簡單,你只需要呼叫 .assymetric 方法,來指定插入( insertion )及移除(removal )的轉場即可。下列是範例程式碼:

.transition(.asymmetric(insertion: .scale(scale: 0, anchor: .bottom), removal: .offset(x: -600, y: 0)))

另外,如果你需要重新使用這個轉場,則可以在 AnyTransition定義一個擴展,如下所示:

extension AnyTransition {
    static var scaleAndOffset: AnyTransition {
        AnyTransition.asymmetric(
            insertion: .scale(scale: 0, anchor: .bottom),
            removal: .offset(x: -600, y: 00)
        )
    }
}

更改程式碼後,請使用內建模擬器來執行這個 App。當紫色矩形出現於螢幕上時,你應該會看到縮放轉場,而當你點擊矩形時,紫色矩形會滑出螢幕畫面。

圖 9.15. 不對稱轉場範例
圖 9.15. 不對稱轉場範例

作業 #1: 使用動畫與轉場建立精美按鈕

現在,你應該具備了轉場與動畫的概念,我們來挑戰建立一個精美的按鈕,以顯示目前操作狀態。請輸入下列網址:https://www.appcoda.com/wp-content/uploads/2019/10/swiftui-animation-16.gif,來看一下效果。

圖 9.16. 精美按鈕
圖 9.16. 精美按鈕

這個按鈕有三種狀態:

  • 原始狀態:以綠色顯示「Submit」按鈕。
  • 處理狀態:顯示一個旋轉的圓環,並更新其標籤為「Processing」。
  • 完成狀態:以紅色顯示「Done」按鈕。

這是一個非常具挑戰性的專案,它將測試你對 SwiftUI 動畫與轉場的了解。你將需要結合到目前為止所學的知識來制定解決方案。

如圖 9.16 所示的範例按鈕,這個處理狀態大約是 4 秒處理狀態約需要 4 秒,你不需要執行實際操作。作為提示,我使用下列的程式碼來模擬該操作。

private func startProcessing() {
    self.loading = true

    // 使用DispatchQueue.main.asyncAfter 來模擬操作
    // 在真實世界的專案中,你將在這裡執行一個任務
    // 當任務完成之後,你將完成狀態設定為true
    DispatchQueue.main.asyncAfter(deadline: .now() + 4) {
        self.completed = true
    }
}

作業 #2: 視圖轉場動畫

你已經學會了如何實作視圖轉場,試著結合在第 5 章中建立的卡片視圖專案,並建立如下所示的視圖轉場。當使用者點擊卡片時,目前的視圖將縮小並淡出,下一個視圖將以放大動畫顯示在前面。

圖 9.17. 視圖轉場動畫
圖 9.17. 視圖轉場動畫

如果你不懂上述的動畫, 則可以輸入網址: https://www.appcoda.com/wp-content/uploads/2019/10/swiftui-view-animation.gif,來看一下動畫效果。

本章小結

動畫在行動 UI 設計中扮演著一個特殊的角色,精心設計的動畫可改善使用者體驗,並讓 UI 的互動具有意義。兩個視圖之間流暢輕快的轉場,將讓使用者滿意且印象深刻。在 App Store 上有超過 200 萬支 App,要使你的 App 脫穎而出,並不容易,不過精心設計的 UI 與動畫肯定會產生根本的差別。

話雖如此,但是對於有經驗的開發者而言,撰寫平滑動畫動畫也不是一件容易的事。幸運的是,SwiftUI 框架簡化了 UI 動畫與轉場的開發,你只須告訴框架:視圖在開始及結束時的狀態為何,SwiftUI 會計算出其餘的狀態,為你渲染出一個流暢且漂亮的動畫。

在本章中,我已介紹了基本的原理,不過如你所見,你已經建立了一些令人愉悅的動畫與轉場效果,最重要的是,只需要幾行程式碼即能辦到。

我期望你喜歡本章的內容,並發掘出有用的技巧。在本章所準備的範例檔中,有完整的專案與作業解答可以下載: