在 iOS 14 使用 matchedGeometryEffect 簡單為 App 建立絢麗的視圖動畫


在 iOS 14 中,Apple 為 SwiftUI 框架引入了很多新功能,像是 LazyVGrid 以及 LazyHGrid。其中 matchedGeometryEffect 非常引人注目,這個功能讓開發者只需要幾行程式碼,就能夠創造絢麗的視圖動畫。SwiftUI 框架已經讓開發者可以簡單地使用動畫來呈現視圖的變化,而 matchedGeometryEffect 修飾器 (modifier) 更將視圖動畫 (view animations) 的實作提升到另一個境界。

對所有手機 App 來說,我們經常需要在多個視圖之間轉換,因此一個令人喜歡的視圖轉換絕對可以提昇整體的使用者體驗。有了 matchedGeometryEffect,你只需要描述兩個視圖的外觀,修飾器就會自動計算兩個視圖的差異,並且自動為大小和位置的變化加上動畫。

可能你會覺得十分困惑,但別擔心,介紹完整個範例 App 之後,你就會明白我在說什麼了。

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

重溫 SwiftUI 動畫

在我們開始介紹 matchedGeometryEffect 之前,讓我們先來看一下如何使用 SwiftUI 來實作動畫。下面的圖片顯示了一個視圖開始和結束的狀態。當你點擊左邊的圓形視圖,它應該會變大且往上移動;相反地,當你點擊右邊的視圖時,它就會回到原本的大小和位置。

要實作這個可點擊的圓形視圖非常簡單。開啟一個新的 SwiftUI 專案後,如此更新 ContentView 結構:

struct ContentView: View {

    @State private var expand = false

    var body: some View {
        Circle()
            .fill(Color.green)
            .frame(width: expand ? 300 : 150, height: expand ? 300 : 150)
            .offset(y: expand ? -200 : 0)
            .animation(.default)
            .onTapGesture {
                self.expand.toggle()
            }
    }
}

我們用一個狀態變數 expand,來記錄 Circle 視圖目前的狀態。當狀態改變時,我們會透過 .frame.offset 這兩個修飾器,來修改視圖框 (frame) 的大小和位移 (offset)。如果在預覽畫面中執行這個 App ,你應該可以在點擊圓形視圖時看到動畫效果。

swiftui-matchedgeometryeffect-animation-circle

了解 matchedGeometryEffect 修飾器

那麼到底什麼是 matchedGeometryEffect 呢?這個功能如何簡化實作視圖動畫的步驟?讓我們再來看一下第一張圖片、以及圓形視圖動畫的程式碼。我們需要找出開始及結束狀態時的確切數值差異。在這個例子當中,就是視圖框的大小及位移。

有了 matchedGeometryEffect 修飾器,你不再需要找出兩個狀態之間的差異了。你只需要描述兩個視圖:一個是開始的狀態,而另一個是結束的狀態matchedGeometryEffect 會自動添加在兩個視圖之間大小和位置的差異。

要使用 matchedGeometryEffect 建立跟之前一樣的動畫效果,你需要先宣告一個命名空間 (namespace) 變數:

@Namespace private var shapeTransition

然後,如此重寫 body 的部分:

var body: some View {
    if expand {

        // Final State
        Circle()
            .fill(Color.green)
            .matchedGeometryEffect(id: "circle", in: shapeTransition)
            .frame(width: 300, height: 300)
            .offset(y: -200)
            .animation(.default)
            .onTapGesture {
                self.expand.toggle()
            }

    } else {

        // Start State
        Circle()
            .fill(Color.green)
            .matchedGeometryEffect(id: "circle", in: shapeTransition)
            .frame(width: 150, height: 150)
            .offset(y: 0)
            .animation(.default)
            .onTapGesture {
                self.expand.toggle()
            }
    }
}

在這段程式碼中,我們建立了兩個圓形視圖,一個表示開始狀態,而另一個表示結束狀態。當它一開始被初始化之後,我們會得到一個 Circle 視圖,位置置中以及寬度為 150 點。當 expand 狀態變數從 false 變成 true 時, App 會顯示另一個 Circle 視圖,其位置是從中間往上 200 點以及寬度為 300 點。

對於兩個 Circle 視圖,我們都添加了 matchedGeometryEffect 修飾器,並且指定了相同的 ID 和命名空間。透過這樣的設定,SwiftUI 可以計算兩個視圖之間大小和位置的差異,並且添加視圖轉換。隨著後續加上的 animation 修飾器,SwiftUI 框架會自動動畫化視圖轉換。

ID 以及命名空間的用途,是用來標記屬於同一個轉換的視圖,所以兩個 Circle 視圖會使用相同的 ID 和命名空間。

以上我們介紹了如何使用 matchedGeometryEffect,來實作兩個視圖之間的動畫轉換效果。如果你有使用過Keynote 的 Magic Move,這個新的修飾器跟 Magic Move非常類似。我建議你使用 iPhone 模擬器來執行這個 App,以便測試這個動畫效果。在我寫這篇文章時, Xcode 12 中有一個 bug,導致我們無法在預覽畫面測試這個動畫效果。

從圓形變為圓角長方形

現在,讓我們來試試實作另一個視圖轉換動畫。這一次,我們會將一個圓形變化成為一個圓角長方形 (rounded rectangle)。圓形位置於螢幕的上端,而圓角長方形則位於螢幕的底端。

swiftui-matchedgeometryeffect-morphing

我們可以使用剛剛學到的技巧,準備兩個視圖:一個圓形視圖和一個圓角長方形視圖,matchedGeometryEffect 修飾器就會處理視圖轉換的部分。現在,讓我們將 body 變數的 ContentView 結構改成這樣:

VStack {
    if expand {

        // Rounded Rectangle
        Spacer()

        RoundedRectangle(cornerRadius: 50.0)
            .matchedGeometryEffect(id: "circle", in: shapeTransition)
            .frame(minWidth: 0, maxWidth: .infinity, maxHeight: 300)
            .padding()
            .foregroundColor(Color(.systemGreen))
            .animation(.easeIn)
            .onTapGesture {
                expand.toggle()
            }

    } else {

        // Circle
        RoundedRectangle(cornerRadius: 50.0)
            .matchedGeometryEffect(id: "circle", in: shapeTransition)
            .frame(width: 100, height: 100)
            .foregroundColor(Color(.systemOrange))
            .animation(.easeIn)
            .onTapGesture {
                expand.toggle()
            }

        Spacer()
    }
}

我們同樣使用 expand 狀態變數,來切換圓形視圖和圓角長方形視圖。這段程式碼跟之前的範例非常相似,只是我們在這邊加上了 VStackSpacer 來定位視圖。或許你會問,為什麼要使用 RoundedRectangle 來建立圓形物件呢?主要原因是這樣可以讓視圖轉換就會更順暢。

在這兩個視圖中,我們都加上了 matchedGeometryEffect 修飾器,並且指定了一樣的 ID 以及命名空間,我們要做的事就完成了。修飾器會自動比較兩個視圖的差異,並且使用動畫呈現這個改變。如果你在預覽畫面或是 iPhone 模擬器上執行這個 App,會看到視圖在圓形和圓角長方形之間完美的轉換。這就是 matchedGeometryEffect 的威力。

swiftui-matchedgeometryeffect-button-transition

不過,你可能注意到這個修飾器無法執行改變顏色的動畫。沒錯,matchedGeometryEffect 只能用來處理位置和大小的變化。

練習一

讓我們來做一個小小的練習,來測試你對 matchedGeometryEffect 的了解。你的任務是要建立如下圖的動畫視圖轉換。開始時,它是一個橘色的圓形視圖,圓形視圖被點擊後,就會轉換成全螢幕的背景圖。你可以在專案檔中找到完整程式碼。

swiftui-matchedgeometryeffect-full-screen

使用動畫轉換來交換兩個視圖

現在你應該對 matchedGeometryEffect 有了基礎的認識,讓我們繼續來看看它如何幫助我們建立一些絢麗的動畫。在這個範例中,我們會交換兩個圓形視圖的位置,並且套用修飾器來建立順暢的視圖轉換。

swiftui-animation-matchedgeometryeffect-swap

我們會使用一個狀態變數,來儲存交換的狀態,並建立一個命名空間變數給 matchedGeometryEffect 來使用。讓我們在 ContentView 宣告這些參數:

@State private var swap = false

@Namespace private var dotTransition

橘色圓形預設位在螢幕的左邊,而綠色圓形則在螢幕的右邊。當使用者點擊任意一個圓形圖案,就會觸發互換的動畫。使用 matchedGeometryEffect 時,你不用了解交換動畫是如何達成的。要建立視圖轉換,你只需要做到以下事情:

  1. 建立橘色和綠色圓形交換之前的版面配置
  2. 建立兩個圓形在交換之後的版面配置

如果你要將版面配置轉換成程式碼,你可以如此編寫 body 變數:

if swap {

    // After swap
    // Green dot on the left, Orange dot on the right

    HStack {
        Circle()
            .fill(Color.green)
            .frame(width: 30, height: 30)
            .matchedGeometryEffect(id: "greenCircle", in: dotTransition)

        Spacer()

        Circle()
            .fill(Color.orange)
            .frame(width: 30, height: 30)
            .matchedGeometryEffect(id: "orangeCircle", in: dotTransition)
    }
    .frame(width: 100)
    .animation(.linear)
    .onTapGesture {
        swap.toggle()
    }

} else {

    // Start state
    // Orange dot on the left, Green dot on the right

    HStack {
        Circle()
            .fill(Color.orange)
            .frame(width: 30, height: 30)
            .matchedGeometryEffect(id: "orangeCircle", in: dotTransition)

        Spacer()

        Circle()
            .fill(Color.green)
            .frame(width: 30, height: 30)
            .matchedGeometryEffect(id: "greenCircle", in: dotTransition)
    }
    .frame(width: 100)
    .animation(.linear)
    .onTapGesture {
        swap.toggle()
    }
}

我們使用 HStack 來將兩個圓形配置成水平排列,並且利用 Spacer 在兩個圓形中間建立一些空間。當 swap 變數設定成 true 之後,綠色圓形會被放在橘色圓形的左邊。相反地,綠色圓形則會被放在橘色圓形的右邊。

如你所見,我們只需要描述不同狀態下圓形視圖的配置,matchedGeometryEffect 就會處理餘下的事情。我們在每個 Circle 視圖加上修飾器,不過這次有一點不同,因為我們有兩個不同的 Circle 視圖需要配置,我們使用了兩個不同的 ID 來建立 matchedGeometryEffect 修飾器。我們將橘色圓形的識別名稱設定為 orangeCircle,而綠色圓形則是設定為 greenCircle

現在,如果你在模擬器執行這個 App,應該可以在點擊任何一個圓形時看到互換的動畫。

練習二

在剛剛的練習中,我們在兩個圓形上使用了 matchedGeometryEffect,並交換它們的位置。這個練習會使用到一樣的技巧,不過這次是要使用兩張圖片。下面的圖展示了這個範例,點擊交換按鈕時,這個 App 就會用漂亮的動畫來交換這兩張圖片。

swiftui-swap-photos

你可以隨意使用自己的圖片。我在範例中使用了這些 Unsplash.com 的免費圖片:

總結

引進 matchedGeometryEffect 修飾器之後,視圖動畫的實作提昇到了另一個層次。你可以用更少的程式碼來創造漂亮的視圖動畫。即便你是 SwiftUI 的新手,你也可以從這個新修飾器中得益,並且讓你的 App 更棒。

編者備註:本文摘自 Mastering SwiftUI 一書。如果你想學習更多不同的視圖動畫及參考其原始碼,請到 AppCoda 網站 購買完整書本。這本書已經為Xcode 12 和 iOS 14 所更新。

譯者簡介:周竑翊 – 只待過新創公司的 iOS 開發者,本職是兩個兒子的爸。有空的時間喜歡看看新的技術跟科技時事。用 Playground 寫寫 Swift,但是 side project 仍然難產。其他興趣喜歡攝影、運動及看電影。歡迎寄信與我聯絡:[email protected]

原文Using matchedGeometryEffect to Create View Animations in iOS 14


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

blog comments powered by Disqus
Shares
Share This