Search code examples
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:

    • A new state variable previousSelectedIndexStr has been added, to record the previous navigation target.

    • The button action saves the current selection as previousSelectedIndexStr, before changing selectedIndexStr.

    • The function viewForSelectedIndex has been tagged as a ViewBuilder. This avoids having to wrap the different kinds of result as AnyView.

    • isSpecialTransitionNeeded is a computed property that determines whether the special transition is needed, based on the previous and next value of selectedIndexStr. This is doing string comparison as you were using before, but I would suggest it might be better to be using an enum here.

    • The top ZStack includes an if statement that determines whether to apply a different transition. This is a fairly primitive but also easy way to switch between different types of transition.

    • The special transition is only used for the insertion transition, the removal transition is always .opacity.

    • An .animation modifier has been added to the ZStack. This controls the speed of the transition. For demo purposes, .gray has also been applied as background to the ZStack.

    • Code that didn't compile in your original example has been commented out or replaced with simple alternatives.

    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