The Util Designer
SwiftUI 自定議畫面跳轉動畫
xcode 13.4.1, swift 5.5, iOS 15.4
2022-08-20
為了令到App的畫面跳轉之間使用動畫,讓使用者知道正在進行甚麼跳轉,比如從列表進入到詳細頁面或從譨細頁面跳回列表頁面,這篇就講講如何做自定議畫面的跳轉。
1. 首先做一個初始畫面。
import SwiftUI

struct CustomTransition : View {
    @State var show = false
    
    var body: some View {
        ZStack {
            if !show {
                VStack {
                    Text("SwiftUI")
                        .font(.largeTitle).fontWeight(.bold)
                        .frame(maxWidth: .infinity, alignment: .leading)
                }
                .padding(20)
                .foregroundColor(.white)
                .background(
                    Color.red
                )
            } else {
            }
        }
    }
}
2. 然後做一個跳轉的結束畫面:
struct CustomTransition : View {
    @State var show = true
    
    var body: some View {
        ZStack {
            if !show {
                VStack {
                    Text("SwiftUI")
                        .font(.largeTitle).fontWeight(.bold)
                        .frame(maxWidth: .infinity, alignment: .leading)
                }
                .padding(20)
                .foregroundColor(.white)
                .background(
                    Color.red
                )
            } else {
                VStack {
                    Spacer()
                    Text("SwiftUI")
                        .font(.largeTitle).fontWeight(.bold)
                        .frame(maxWidth: .infinity, alignment: .leading)
                }
                .padding(20)
                .foregroundColor(.white)
                .background(
                    Color.blue
                )
            }
        }
    }
}
3. 現在我們來改變show的值,就可以切換畫面的轉換:
import SwiftUI

struct CustomTransition : View {
    @State var show = false
    
    var body: some View {
        ZStack {
            if !show {
                VStack {
                    Text("SwiftUI")
                        .font(.largeTitle).fontWeight(.bold)
                        .frame(maxWidth: .infinity, alignment: .leading)
                }
                .padding(20)
                .foregroundColor(.white)
                .background(
                    Color.red
                )
            } else {
                VStack {
                    Spacer()
                    Text("SwiftUI")
                        .font(.largeTitle).fontWeight(.bold)
                        .frame(maxWidth: .infinity, alignment: .leading)
                }
                .padding(20)
                .foregroundColor(.white)
                .background(
                    Color.blue
                )
            }
        }
        .onTapGesture {
            withAnimation {
                show.toggle()
            }
        }
    }
}
4. 雖然使用了withAnimation,但還沒有動畫漸變的效果,其實SwiftUI提供了一個方法@Namespace和matchedGeometryEffect來做到切換頁面時動畫,比如下面程式就是把文字動畫方式從初始位置移到結束位置:
import SwiftUI

struct CustomTransition : View {
    @Namespace var namespace
    @State var show = false
    
    var body: some View {
        ZStack {
            if !show {
                VStack {
                    Text("SwiftUI")
                        .font(.largeTitle).fontWeight(.bold)
                        .frame(maxWidth: .infinity, alignment: .leading)
                        .matchedGeometryEffect(id: "title", in: namespace)
                }
                .padding(20)
                .foregroundColor(.white)
                .background(
                    Color.red
                )
            } else {
                VStack {
                    Spacer()
                    Text("SwiftUI")
                        .font(.largeTitle).fontWeight(.bold)
                        .frame(maxWidth: .infinity, alignment: .leading)
                        .matchedGeometryEffect(id: "title", in: namespace)
                }
                .padding(20)
                .foregroundColor(.white)
                .background(
                    Color.blue
                )
            }
        }
        .onTapGesture {
            withAnimation {
                show.toggle()
            }
        }
    }
}
5. 到現在已經比剛開始增加了動畫的頁面跳轉,以下會把背景色以動畫把紅色漸變成全藍色,而且背景的大小也是以動畫方式慢慢的變成佔滿整個畫面的。
import SwiftUI

struct CustomTransition : View {
    @Namespace var namespace
    @State var show = false
    
    var body: some View {
        ZStack {
            if !show {
                VStack {
                    Text("SwiftUI")
                        .font(.largeTitle).fontWeight(.bold)
                        .frame(maxWidth: .infinity, alignment: .leading)
                        .matchedGeometryEffect(id: "title", in: namespace)
                }
                .padding(20)
                .foregroundColor(.white)
                .background(
                    Color.red.matchedGeometryEffect(id: "background", in: namespace)
                )
            } else {
                VStack {
                    Spacer()
                    Text("SwiftUI")
                        .font(.largeTitle).fontWeight(.bold)
                        .frame(maxWidth: .infinity, alignment: .leading)
                        .matchedGeometryEffect(id: "title", in: namespace)
                }
                .padding(20)
                .foregroundColor(.white)
                .background(
                    Color.blue.matchedGeometryEffect(id: "background", in: namespace)
                )
            }
        }
        .onTapGesture {
            withAnimation {
                show.toggle()
            }
        }
    }
}
6. 若把切換頁面時間拉長,就能更明顯的看到整個過程,實作如下:
import SwiftUI

struct CustomTransition : View {
    @Namespace var namespace
    @State var show = false
    
    var body: some View {
        ZStack {
            if !show {
                VStack {
                    Text("SwiftUI")
                        .font(.largeTitle).fontWeight(.bold)
                        .frame(maxWidth: .infinity, alignment: .leading)
                        .matchedGeometryEffect(id: "title", in: namespace)
                }
                .padding(20)
                .foregroundColor(.white)
                .background(
                    Color.red.matchedGeometryEffect(id: "background", in: namespace)
                )
            } else {
                VStack {
                    Spacer()
                    Text("SwiftUI")
                        .font(.largeTitle).fontWeight(.bold)
                        .frame(maxWidth: .infinity, alignment: .leading)
                        .matchedGeometryEffect(id: "title", in: namespace)
                }
                .padding(20)
                .foregroundColor(.white)
                .background(
                    Color.blue.matchedGeometryEffect(id: "background", in: namespace)
                )
            }
        }
        .onTapGesture {
            withAnimation(.linear(duration: 2)) {
                show.toggle()
            }
        }
    }
}
7. 再試試把結束畫面作為詳細畫面,比如SwiftUI的詳細介紹,效果會更明顯:
import SwiftUI

struct CustomTransition : View {
    @Namespace var namespace
    @State var show = false
    
    var body: some View {
        ZStack {
            if !show {
                VStack {
                    Text("SwiftUI")
                        .font(.largeTitle).fontWeight(.bold)
                        .frame(maxWidth: .infinity, alignment: .leading)
                        .matchedGeometryEffect(id: "title", in: namespace)
                }
                .padding(20)
                .foregroundColor(.white)
                .background(
                    Color.red.matchedGeometryEffect(id: "background", in: namespace)
                )
            } else {
                VStack {
                    Spacer()
                    Text("SwiftUI provides views, controls, and layout structures for declaring your app’s user interface. The framework provides event handlers for delivering taps, gestures, and other types of input to your app, and tools to manage the flow of data from your app’s models down to the views and controls that users will see and interact with.")
                    Spacer()
                    Text("SwiftUI")
                        .font(.largeTitle).fontWeight(.bold)
                        .frame(maxWidth: .infinity, alignment: .leading)
                        .matchedGeometryEffect(id: "title", in: namespace)
                }
                .padding(20)
                .foregroundColor(.white)
                .background(
                    Color.blue.matchedGeometryEffect(id: "background", in: namespace)
                )
            }
        }
        .onTapGesture {
            withAnimation(.linear(duration: 2)) {
                show.toggle()
            }
        }
    }
}