SwiftUI 動畫入門教學: 建立一個下載指示器


你是否曾在 Keynote 使用過奇妙的動作動畫?有了這些奇妙的動作,你可以輕易的建立兩張投影片間的滑動動畫 (slick animation)。 Keynote 會自動地分析兩張投影片間的物件,然後自動地渲染動畫。同樣,SwiftUI 也將奇妙動作 (Magic Move) 動畫帶入了 App 的開發中。動畫所用的框架是自動且神奇的。你只要定義一個視圖的兩個狀態,SwiftUI 會自動進行計算,接著以動畫的方式來呈現狀態之間的變化。

SwiftUI 可以讓你針對個別視圖內的變化以動畫來呈現,也可以實作視圖之間的轉場 (transition) 動作。

在本教學文,你將學習如何使用 SwiftUI 所提供的隱式 (implicit) 與顯式 (explicit) 動畫。另外,我們會用幾個範例專案來學習這些程式技巧並建立下載指示器 (Loading Indicator)。

編者備註:本文摘自 Mastering SwiftUI 一書。如果你想更深入學習 SwiftUI 動畫 和 SwiftUI 框架,請到 AppCoda 網站購買完整書本。

隱式與顯示動畫

SwiftUI 提供兩種動畫類型:隱式與顯式。這兩個方法可以產生視圖動畫與視圖間的轉場效果。這個框架提供一個稱作 animation 的框架來實作隱式動畫。把這個修飾器加到你想要呈現動畫的視圖上,並指定你所需要到動畫類型,另外,你可以定義動畫持續時間與延遲時間。SwiftUI 會依照視圖狀態的變化自動渲染動畫。

顯式動畫提供精緻的動畫控制讓你呈現想要的動畫效果。顯示動畫不是將修飾器貼到視圖上,而是在 withAnimation() 區塊內,告訴 SwiftUI ,什麼樣的狀態做改變時,你想要呈現什麼樣的動畫。

還是搞不清楚嗎?沒有關係,練習幾個範例你就會更有概念了。

隱式動畫

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

SwiftUIAnimation-1
圖 1. 按鈕狀態改變時的動畫

以圖 1 來看,這個視圖很簡單,可以點擊,由紅色圓與心形組成。當一個使用者點擊心形或者圓,圓的顏色會變成淡灰色,心形則會變成紅色,同時間心形的也會變得大一點,因此,我們有幾個狀態會做變化:

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

如果你使用 SwiftUI 來實作可點擊的圓,程式內容如下所示:

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 {
            self.circleColorChanged.toggle()
            self.heartColorChanged.toggle()
            self.heartSizeChanged.toggle()
        }

    }
}

我們定義了三個狀態變數來建立狀態模型,初始值設為 false。圓與心形則使用 ZStack 來建立,將心形疊加在圓形上面。 SwiftUI 有一個 onTapGesture 修飾器可以偵測手勢。你可以將它附加上任何視圖要做點擊的地方。在 onTapGesture 閉包中,我們使用 toggle() 來開啟狀態以改變視圖的外觀。

SwiftUIAnimation-2
圖 2. 實作圓與心形視圖

倘若你在畫布執行這個 App,當你點擊視圖時,這個圓與心形的圖示會做改變,不過這些變化沒有動畫。

要讓變化呈現動畫效果,你需要加上一個 animation 修飾器至 CircleImage 視圖:

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

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

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)
.onTapGesture {
    self.circleColorChanged.toggle()
    self.heartColorChanged.toggle()
    self.heartSizeChanged.toggle()
}

執行結果是一樣的, SwiftUI 尋找嵌入在 ZStack 中所有的改變狀態,並建立動畫。

在範例中,我們使用預設動畫,SwiftUI 提供了幾個內建動畫供選擇,其中包括了 lineareaseIn,、easeOuteaseInOutspring。線性動畫 (linear) 動畫是以線性速度來呈現變化,而緩動動畫 (ease easing animations) 則速度會做變化。細節部分你可以參考 www.easings.net 來了解每一個 ease 函數的不同之處。

要使用其他的動畫,你只需要在動畫修飾器中設定指定的動畫。譬如說,想要使用一個 spring 動畫,你可以將 .default 值變更如下:

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

這會渲染一個以彈性動畫 (spring animation),讓心形有一個心跳的特效。你可以調整阻尼 (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(nil) 加到視圖中來防止 SwiftUI 產生狀態變化時的動畫。以下為達到同樣效果的程式:

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

    Image(systemName: "heart.fill")
        .foregroundColor(heartColorChanged ? .red : .white)
        .font(.system(size: 100))
        .animation(nil) // 從此處取消動畫
        .scaleEffect(heartSizeChanged ? 1.0 : 0.5)
        .animation(.spring(response: 0.3, dampingFraction: 0.3, blendDuration: 0.3))
}
.onTapGesture {
    self.circleColorChanged.toggle()
    self.heartColorChanged.toggle()
    self.heartSizeChanged.toggle()
}

我們在 scaleEffect 之前插入 animation(nil) 修飾器。這會將動畫取消, scaleEffect 修飾器的狀態改變動畫將不會再發生。

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

使用 RotationEffect 建立一個下載指示器

SwiftUI 的動畫的威力是你不需要去了解這些視圖動畫是如何產生,你只需要提供起始與結束狀態。SwiftUI 會幫忙解決後續的工作。如果你具備了這個觀念,你便可以建立各式不同類型的動畫。

SwiftUI-動畫-1
圖 3. 下載指示器範例

舉例來說,我們建立一個簡單的下載指示器,這個指示器在市面上的應用很常見,像是 Medium ,要建立一個如上圖的指示器,我們一開始建立一個開口圓,程式如下所示:

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(Animation.default.repeatForever(autoreverses: false))
            .onAppear() {
                self.isLoading = true
            }
    }
}

rotationEffect 修飾器有旋轉角度的參數設定,在上面的程式中,我們有一個狀態變數作為控制下載狀態用。當它設定為 true 時,這個旋轉角度將會以 360 度來轉動圓。在 animation 修飾器,我們指定使用預設動畫,不過還有些不同,我們告訴 SwiftUI 要一遍又一遍重複同樣的動畫,這是建立下載動畫的關鍵部分。

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

Animation.linear(duration: 1).repeatForever(autoreverses: false)

值設的越大,則動畫越慢。

這裡的 onAppear 修飾器對你來說可能比較陌生,如果你對 UIKit 有些理解的話,這個修飾器跟 viewDidAppear 非常相似。當視圖出現在畫面時會自動呼叫。在這個程式中,當視圖載入時,為了啟動這個動畫,我們變更下載狀態為 true。

一旦你熟悉了這個技術,你可以調整設計並開發各種不同版本的下載指示器。譬如說,你可以疊加一個圓弧在圓圈上面來建立酷炫的指示器。

SwiftUI-動畫-2

程式碼如下所示:

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(Animation.linear(duration: 1).repeatForever(autoreverses: false))
                .onAppear() {
                    self.isLoading = true
            }
        }
    }
}

這個下載指示器,不一定是要圓形,你也可以使用 RectangleRoundedRectangle 來建立指示器。不過這裡不變更旋轉角度,你可以修改偏移 (offset) 值來建立如下圖的動畫。

SwiftUI-動畫-3
圖 5. 下載指示器的另一個範例

我們將兩個圓角矩形疊在一起來建立這個動畫。上面的矩形比下面的短,當載入開始時,我們將 offset 值從 -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(Animation.linear(duration: 1).repeatForever(autoreverses: false))
        }
        .onAppear() {
            self.isLoading = true
        }
    }
}

這會讓上面的矩形沿著線移動。另外,當你一遍又一遍重複同樣的動畫,它會變成一個載入動畫,下圖為偏移植的說明。

SwiftUI-動畫-4
圖 6. 下載指示器的另一個範例

總結

即使有經驗的開發者,要處理一個滑動動畫也不是一件容易的事。很幸運地,SwiftUI 框架簡化了 UI 動畫與轉場開發過程。你只要告訴框架視圖在開始與結束該怎麼做即可。SwiftUI 會處理剩下的任務,為你渲染出一個流暢且漂亮的動畫。

在此教學文,我已經介紹了基本的原理,不過如你所見,你已經建立了一些漂亮的動畫與轉場效果。更重要的是,只要幾行程式即能辦到。

編者備註:本文摘自 Mastering SwiftUI 一書。如果你想更深入學習 SwiftUI 動畫 和 SwiftUI 框架,請到 AppCoda 網站購買完整書本。

譯者簡介:王豪勳 -渥合數位服務創辦人,畢業於台灣大學應用力學研究所,曾在半導體產業服務多年,近年來專注於協助客戶進行 App 軟體以及網站開發,平常致力於研究各式最軟硬體技術,擁有多本譯作。


軟件工程師,AppCoda 創辦人。著有《iOS 13 App 程式設計實力超進化實戰攻略》、《iOS 13 App 程式設計實力超進化實戰攻略》以及《精通 SwiftUI》。曾任職於HSBC, FedEx等公司,專責軟體開發、系統設計。2012年創立AppCoda技術部落格,定期發表iOS程式教學文章。現時專注發展AppCoda,致力於iOS程式教學,產品設計及開發。

blog comments powered by Disqus
Shares
Share This