如何在 SwiftUI 中点击按钮时导航视图时添加动画

问题描述 投票:0回答:1

如何获得这样的动画

enter image description here

代码:使用此代码,我可以正常导航视图,但如果我在

AttendanceSwipeView(viewModel: viewModel)
,那么如果我点击仪表板,那么视图应该带有这种动画(就像从下到前的动画)...

我的意思是仪表板>出勤>摘要...导航可以正常但是

从 Feed 到出勤,再到出勤到仪表板...这里需要自下而上的动画。如果可能,请指导我如何实现这一点。

struct DashboardBottomView: View {
    @StateObject var viewModel: AppDashboardViewModel
    @State var selectedClassInd: Int = 0
    @State private var stringArray = ["Dashboard", "Attendance", "Feed"]
    @State private var selectedIndex: Int? = nil
    @State private var selectedIndexStr: String? = "Dashboard"
    
    
    private func viewForSelectedIndex() -> some View {
        switch selectedIndexStr {
        case "Dashboard":
            return AnyView(DashboardView(viewModel: viewModel))
        case "Attendance":
            return AnyView(AttendanceSwipeView(viewModel: viewModel))
        case "Feed":
            return AnyView(DashboardFeedView())
        default:
            return AnyView(Text("Default View"))
        }
    }
    
    var body: some View {
        
        VStack(spacing: 0) {
            Spacer()
            
            ZStack {
                viewForSelectedIndex()
                    .toolbar(.hidden, for: .navigationBar)

            }
            
            ZStack{
                Color.appGreen2
                ScrollView(.horizontal, showsIndicators: false) {
                    LazyHGrid(rows: [GridItem(.flexible(), spacing: 0)], spacing: 0) {
                        ForEach(stringArray, id: \.self) { data in
                            Button {
                                withAnimation {
                                    selectedIndexStr = data
                                }
                            }
                        label: {
                            VStack {
                                Text(data)
                                    .font(.calibriBold(with: 14))
                                    .foregroundColor(Color.white)
                                    .padding(.horizontal, 8)
                                if selectedIndexStr == data {
                                    gradientDivider
                                        .frame(height: 2)
                                }
                            }
                            .frame(height: 40)
                            .frame(minWidth: 108)
                            .animation(.default, value: selectedClassInd)
                        }
                        .buttonStyle(.plain)
                        }
                    }
                    .padding(.bottom, 0)
                    .background(Color.appGreen2)
                }
                .frame(height: 55)
            }
        }
        .ignoresSafeArea()
        
        .onAppear {
            viewModel.fetchDashboardData { status in
                if status {
                    stringArray = viewModel.dashboardButtonsArray
                    selectedIndexStr = stringArray.first
                }
            }
        }
    }
}
animation swiftui view uinavigationcontroller uibutton
1个回答
0
投票

看起来我们在这里讨论的是自定义过渡。

自定义过渡可用于将

ViewModifier
应用于处于
active
(之前)和
identity
(之后)状态的视图。当转换为动画时,两个状态之间会自动插值。这对于简单的线性过渡来说很好,就像这里的情况一样。

以下

ViewModifier
将不透明度、旋转和偏移效果应用于视图:

struct SwipeMovement: ViewModifier {
    let progress: Double

    func body(content: Content) -> some View {
        content
            .opacity(progress)
            .offset(y: -400)
            .rotationEffect(.degrees((progress - 1) * 30))
            .offset(y: 400 + ((1 - progress) * 100))
    }
}

然后将自定义过渡作为

extension
添加到
AnyTransition
:

extension AnyTransition {
    static var swipeMovement: AnyTransition {
        .modifier(
            active: SwipeMovement(progress: 0),
            identity: SwipeMovement(progress: 1)
        )
    }
}

现在自定义过渡就可以使用了。下面的代码是您的示例的改编版本,具有以下更改:

  • 添加了新的状态变量

    previousSelectedIndexStr
    ,用于记录之前的导航目标。

  • 函数

    viewForSelectedIndex
    已被标记为
    ViewBuilder
    。这避免了必须将不同类型的结果包装为
    AnyView

  • isSpecialTransitionNeeded
    是一个计算属性,它根据
    selectedIndexStr
    的上一个和下一个值确定是否需要特殊转换。这是像您之前使用的那样进行字符串比较,但我建议在这里使用枚举可能会更好。

  • 顶部

    ZStack
    包含一个
    if
    语句,用于确定是否应用不同的转换。这是一种相当原始但也是在不同类型的过渡之间切换的简单方法。

  • 特殊过渡仅用于

    insertion
    过渡,
    removal
    过渡始终为
    .opacity

  • .animation
    修饰符已添加到
    ZStack
    。这控制了转换的速度。

  • 按钮回调将当前选择记录为

    previousSelectedIndexStr
    ,然后更改
    selectedIndexStr

  • 原始示例中未编译的代码已被注释掉或替换为简单的替代方案。

struct DashboardBottomView: View {
//    @StateObject var viewModel: AppDashboardViewModel
    @State var selectedClassInd: Int = 0
    @State private var stringArray = ["Dashboard", "Attendance", "Feed"]
    @State private var selectedIndex: Int? = nil
    @State private var previousSelectedIndexStr: String?
    @State private var selectedIndexStr: String? = "Dashboard"

    @ViewBuilder
    private func viewForSelectedIndex() -> some View {
        switch selectedIndexStr {
        case "Dashboard":
            Text("DashboardView")
                .frame(maxWidth: .infinity, maxHeight: .infinity)
        case "Attendance":
            Text("AttendanceSwipeView")
                .frame(maxWidth: .infinity, maxHeight: .infinity)
                .background(.yellow)
        case "Feed":
            Text("DashboardFeedView")
                .frame(maxWidth: .infinity, maxHeight: .infinity)
                .background(.orange)
        default:
            Text("Default View")
                .frame(maxWidth: .infinity, maxHeight: .infinity)
                .background(.red)
        }
    }

    private var isSpecialTransitionNeeded: Bool {
        if let previousSelectedIndexStr, let selectedIndexStr {
            (previousSelectedIndexStr == "Feed" && selectedIndexStr == "Attendance") ||
            (previousSelectedIndexStr == "Attendance" && selectedIndexStr == "Dashboard")
        } else {
            false
        }
    }

    var body: some View {
        VStack(spacing: 0) {
            Spacer()

            ZStack {
                if isSpecialTransitionNeeded {
                    viewForSelectedIndex()
                        .transition(
                            .asymmetric(
                                insertion: .swipeMovement,
                                removal: .opacity
                            )
                        )
                } else {
                    viewForSelectedIndex()
                        .transition(.opacity)
                }
            }
            .animation(.easeInOut(duration: 1), value: selectedIndexStr)
            .toolbar(.hidden, for: .navigationBar)

            ZStack{
                Color.green //appGreen2
                ScrollView(.horizontal, showsIndicators: false) {
                    LazyHGrid(rows: [GridItem(.flexible(), spacing: 0)], spacing: 0) {
                        ForEach(stringArray, id: \.self) { data in
                            Button {
                                previousSelectedIndexStr = selectedIndexStr
                                withAnimation {
                                    selectedIndexStr = data
                                }
                            }
                        label: {
                            VStack {
                                Text(data)
//                                    .font(.calibriBold(with: 14))
                                    .foregroundColor(Color.white)
                                    .padding(.horizontal, 8)
//                                if selectedIndexStr == data {
//                                    gradientDivider
//                                        .frame(height: 2)
//                                }
                            }
                            .frame(height: 40)
                            .frame(minWidth: 108)
                            .animation(.default, value: selectedClassInd)
                        }
                        .buttonStyle(.plain)
                        }
                    }
                    .padding(.bottom, 0)
                    .background(Color.green) // appGreen2
                }
                .frame(height: 55)
            }
        }
        .ignoresSafeArea()

//        .onAppear {
//            viewModel.fetchDashboardData { status in
//                if status {
//                    stringArray = viewModel.dashboardButtonsArray
//                    selectedIndexStr = stringArray.first
//                }
//            }
//        }
    }
}

Animation

© www.soinside.com 2019 - 2024. All rights reserved.