animationswiftuiviewuinavigationcontrolleruibutton

How to add animation while navigate views when tap on button in SwiftUI


How to get animation like this

enter image description here

Code: with this code i am able to navigate views normally but if i am in AttendanceSwipeView(viewModel: viewModel) then if i tap on Dashboard then view should come with this kind of animation(like something from bottom back to front)...

i mean Dashboard > Attendance > Feed ... navigation can be normal but

from Feed to Attendance and Attendance to Dashboard... here need down to top kind of animation. if possible please guide me how to achieve this.

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
                }
            }
        }
    }
}

Solution

  • A custom transition could be used here.

    A custom transition can apply a ViewModifier to the view in its active (before) and identity (after) state. When the transition is animated, there is automatic interpolation between the two states. This is fine for a simple linear transition, as seems to be the case here.

    The following ViewModifier applies opacity, rotation and offset effects to the view:

    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))
        }
    }
    

    The custom transition can be defined as an extension to AnyTransition:

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

    Now the custom transition is ready to be used. The code below is an adapted version of your example, with the following changes:

    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)
                    .background(.background)
            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)
                    }
                }
                .background(.gray)
                .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