Search code examples
swiftuiswiftui-tabview

`TabView` and `tabViewStyle(.page)` not navigating as expected


I've been experimenting with TabView and tabViewStyle and I've run into a problem with my code I can't figure out.

In the code below, when the app opens up on my device I start on the HomeScreen() (as expected) but if I tap on Profile in the top bar, the tab navigation doesn't happen. The Profile text turns red (indicating that pageIndex has been updated), but for reasons I can't figure out, the TabView isn't updating accordingly.

BUT, if I open the app and tap on Settings in the top bar, the tab navigation happens as expected.

Swiping works as expected, no issues there.

Have I missed something obvious?

Steps to reproduce:

  1. Copy code into xcode
  2. Run on simulator / canvas / device
  3. Tap Profile (don't swipe or tap anything else)
  4. Profile will turn red, but the page won't be animated left to the Profile screen.
  5. If you tap Settings or swipe any direction, tapping Profile will work as expected.
import SwiftUI

struct SwipeNavigation2: View {
    @State var pageIndex = 1
    
    var body: some View {
        NavigationView {
            TabView(selection: self.$pageIndex) {
                // The screen to the "left" of the Home screen
                ProfileScreen()
                    .tag(0)
                
                // The screen we want the app to load on
                HomeScreen()
                    .tag(1)
                
                // The screen to the "right" of the Home screen
                SettingsScreen()
                    .tag(2)
            }
            .tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
            .navigationTitle("")
            .navigationBarTitleDisplayMode(.inline)
            .toolbar {
                ToolbarItem(placement: .navigationBarLeading) {
                    Button {
                        withAnimation(.spring()) {
                            pageIndex = 0
                        }
                    } label: {
                        Text("Profile")
                            .foregroundColor(pageIndex == 0 ? .red : .primary)
                    }
                }
                ToolbarItem(placement: .navigationBarTrailing) {
                    Button {
                        withAnimation(.spring()) {
                            pageIndex = 2
                        }
                    } label: {
                        Text("Settings")
                            .foregroundColor(pageIndex == 2 ? .red : .primary)
                    }
                }
            }
        }
    }
}

private struct ProfileScreen: View {
    var body: some View {
        Text("Profile screen")
    }
}

private struct HomeScreen: View {
    var body: some View {
        Text("Home screen")
    }
}

private struct SettingsScreen: View {
    var body: some View {
        Text("Settings screen")
    }
}

Edit:

I've taken some of the suggestions and amended the code as such:

struct SwipeNavigation2: View {
    @State var pageIndex = 0
    
    var body: some View {
        NavigationView {
            TabView(selection: self.$pageIndex) {
                ProfileScreen()
                    .tag(0)
                
                HomeScreen()
                    .tag(1)
                
                SettingsScreen()
                    .tag(2)
            }
            .onAppear {
                pageIndex = 1
            }
            .tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
            .navigationTitle("")
            .navigationBarTitleDisplayMode(.inline)
            .toolbar {
                ToolbarItem(placement: .navigationBarLeading) {
                    Button {
                        withAnimation(.spring()) {
                            pageIndex = 0
                        }
                    } label: {
                        Text("Profile")
                            .foregroundColor(pageIndex == 0 ? .red : .primary)
                    }
                }
                ToolbarItem(placement: .navigationBarTrailing) {
                    Button {
                        withAnimation(.spring()) {
                            pageIndex = 2
                        }
                    } label: {
                        Text("Settings")
                            .foregroundColor(pageIndex == 2 ? .red : .primary)
                    }
                }
            }
        }
    }
}

Edit 1:

Here's a recording from my simulator (Xcode14.1), on an iPhone 14. You'll see once the recording starts, I tap on Profile (which turns it red), but the TabView isn't moving me to the correct page.

https://i.sstatic.net/2uJ0c.jpg

Edit 2:

It gets weirder. I've tested the following devices in XCode simulator:

  • iPhone 13 (doesn't work)
  • iPhone 13 Mini (doesn't work)
  • iPhone 14 (doesn't work)
  • iPhone 14 Pro (works)
  • iPhone 14 Pro Max (works)

Solution

  • Move the onAppear modifier on the tab with the index set at init (in your case the Profile View.

    import SwiftUI
    
    struct AdamView: View {
        @State var pageIndex: Int = 0
    
        var body: some View {
            NavigationView {
               TabView(selection: self.$pageIndex) {
                Text("Profile")
                    .onAppear {
                        pageIndex = 1
                    }
                    .tag(0)
                
                Text("Home")
                    .tag(1)
                
                Text("Settings")
                    .tag(2)
            }
            .tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
            .navigationTitle("")
            .navigationBarTitleDisplayMode(.inline)
            .toolbar {
                ToolbarItem(placement: .navigationBarLeading) {
                    Button {
                        withAnimation(.spring()) {
                            pageIndex = 0
                        }
                    } label: {
                        Text("Profile")
                            .foregroundColor(pageIndex == 0 ? .red : .primary)
                    }
                }
                ToolbarItem(placement: .navigationBarTrailing) {
                    Button {
                        withAnimation(.spring()) {
                            pageIndex = 2
                        }
                    } label: {
                        Text("Settings")
                            .foregroundColor(pageIndex == 2 ? .red : .primary)
                    }
                }
              }
           }
        }
     }
    

    Another solution is to use the task modifier instead. With the task, you can keep the onAppear on the TabView but I noticed we see the Profile View for a very short time before showing the Home tab.