Animate/transition tab item appearance in TabView

Hello all,

Could someone please help me figure out a wait to animate switching between the two views that are items in the TabView below? Ideally, I’d like the transition to be something natural, e.g. swiping left while in CurrentContentView moves it to the left and drags PreviousContentView from the right, and vice versa when swithing back.

import SwiftUI

struct ContentView: View {

    @State private var selectedTab = 0
    let numTabs = 2
    let minDrag: CGFloat = 50
    
    // MARK: - BODY
    var body: some View {

        let currentDay = Calendar.current.component(.day, from: Date())

        TabView(selection: $selectedTab) {
            Group{
                CurrentContentView()
                    .tabItem {
                        VStack{
                            Image(systemName: "\(currentDay).circle.fill")
                            Text("Current content")
                        }
                    }
                    .tag(0)
                    .highPriorityGesture(
                        DragGesture()
                            .onEnded({
                                self.handleSwipe(translation: $0.translation.width)
                            })
                    )

                PreviousContentView()
                    .tabItem {
                        VStack{
                            Image(systemName: "clock.arrow.circlepath")
                            Text("Previous content")
                        }
                    }
                    .tag(1)
                    .highPriorityGesture(
                        DragGesture()
                            .onEnded({
                                self.handleSwipe(translation: $0.translation.width)
                            })
                    )
            }
            .toolbar(.visible, for: .tabBar)
            .toolbarBackground(Color.white, for: .tabBar)
        }
    }
    
    private func handleSwipe(translation: CGFloat) {
        if translation > minDrag && selectedTab > 0 {
            selectedTab -= 1
            edge = .leading
        }
        else if translation < -minDrag && selectedTab < numTabs - 1 {
            selectedTab += 1
            edge = .trailing
        }
    }
    
}

To animate the transition between two views in a TabView, you can add a custom transition using the transition modifier and AnyTransition type.

Here is an updated implementation:

struct ContentView: View {

    @State private var selectedTab = 0
    let numTabs = 2
    let minDrag: CGFloat = 50
    @State private var edge = Edge.leading
    
    // MARK: - BODY
    var body: some View {

        let currentDay = Calendar.current.component(.day, from: Date())

        TabView(selection: $selectedTab) {
            Group{
                CurrentContentView()
                    .tabItem {
                        VStack{
                            Image(systemName: "\(currentDay).circle.fill")
                            Text("Current content")
                        }
                    }
                    .tag(0)
                    .highPriorityGesture(
                        DragGesture()
                            .onEnded({
                                self.handleSwipe(translation: $0.translation.width)
                            })
                    )
                .transition(AnyTransition.move(edge: edge).combined(with: .opacity))

                PreviousContentView()
                    .tabItem {
                        VStack{
                            Image(systemName: "clock.arrow.circlepath")
                            Text("Previous content")
                        }
                    }
                    .tag(1)
                    .highPriorityGesture(
                        DragGesture()
                            .onEnded({
                                self.handleSwipe(translation: $0.translation.width)
                            })
                    )
                .transition(AnyTransition.move(edge: edge).combined(with: .opacity))
            }
            .toolbar(.visible, for: .tabBar)
            .toolbarBackground(Color.white, for: .tabBar)
        }
    }
    
    private func handleSwipe(translation: CGFloat) {
        if translation > minDrag && selectedTab > 0 {
            selectedTab -= 1
            edge = .leading
        }
        else if translation < -minDrag && selectedTab < numTabs - 1 {
            selectedTab += 1
            edge = .trailing
        }
    }
    
}

Hi @joash,

Thank you for taking the time to help me. Try as I might, though, I couldn’t get your code to produce any sort of visible effect - and I tried both on the simulator and on my phone.

Hi @d3f7

Can you confirm if this is the behaviour you’re trying to accomplish?

As a user I want to be able to swipe left and right to change the current tab view with the ability also to tap the tab icon to also change from current view to the next view as shown on the GIF attachment below.

Simulator Screen Recording - iPhone 14 Pro - 2023-02-01 at 12.51.42

I’m not able to run the code you shared so I assume that this is the behaviour you’re trying to achieve.

Hi @joash,

Here’s the effect I was trying to achieve (the sliding left-right between the child views):
animation

Ultimatelly, I constructed a custom tab view and applied the transitions there. Here’s the code:

import SwiftUI

struct ContentView: View {

    @State private var showTab = 0
    
    let numTabs = 2
    let minDrag: CGFloat = 50
    let animationDuration = 0.2
    
    let activeTint = Color("vpurple")
    let inactiveTint = Color.primary
    
    @State private var dailyTint: Color = Color("vpurple")
    @State private var previousTint: Color = .primary
    
    var body: some View {
        
        let currentDay = Calendar.current.component(.day, from: Date())
        
        VStack {
            
            if showTab == 0 {
                DailyAphorismView()
                    .transition(AnyTransition.asymmetric(insertion: .push(from: .leading), removal: .push(from: .trailing)).combined(with: .opacity))
                    .highPriorityGesture(
                        DragGesture()
                            .onEnded({
                                self.handleSwipe(translation: $0.translation.width)
                            })
                    )
            }
            else {
                PreviousAphorismsView()
                    .transition(AnyTransition.asymmetric(insertion: .push(from: .trailing), removal: .push(from: .leading)).combined(with: .opacity))
                    .highPriorityGesture(
                        DragGesture()
                            .onEnded({
                                self.handleSwipe(translation: $0.translation.width)
                            })
                    )
            }
            
            Spacer()
            
            HStack {
                
                Button {
                    withAnimation(.easeIn(duration: animationDuration)) {
                        showTab = 0
                        previousTint = inactiveTint
                        dailyTint = activeTint
                    }
                } label: {
                    VStack{
                        Image(systemName: "\(currentDay).circle.fill")
                            .font(.title2)
                            .padding(2)
                        Text("Афоризъм на деня")
                            .font(.caption)
                    }
                }
                .tint(dailyTint)
                .padding(.horizontal, 20)
                
                Spacer()

                Button {
                    withAnimation(.easeIn(duration: animationDuration)) {
                        showTab = 1
                        dailyTint = inactiveTint
                        previousTint = activeTint
                    }
                } label: {
                    VStack{
                        Image(systemName: "clock.arrow.circlepath")
                            .font(.title2)
                            .padding(2)
                        Text("Предишни афоризми")
                            .font(.caption)
                    }
                }
                .tint(previousTint)
                .padding(.horizontal, 20)
                

            }
            .tint(.primary)
            .padding(.vertical, 5)
            .padding(.horizontal, 20)
        }
    }
    
    private func handleSwipe(translation: CGFloat) {
        withAnimation(.easeIn(duration: animationDuration)) {
            if translation > minDrag && showTab > 0 {
                showTab -= 1
                previousTint = inactiveTint
                dailyTint = activeTint
            }
            else if translation < -minDrag && showTab < numTabs - 1 {
                showTab += 1
                dailyTint = inactiveTint
                previousTint = activeTint
            }
        }
    }
    
}

This is nice @d3f7

So just to confirm, your issue is now solve, is that correct?

Indeed it is.

1 Like