3

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.stack.imgur.com/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)
ragavanmonke
  • 409
  • 3
  • 13
  • 1
    Might be `SwiftUI` bug. For a quick fix, you can initialize `pageIndex` with `0` and set it to `1` in `onAppear`. – Nirav D Jan 12 '23 at 18:32
  • @NiravD Thanks for the suggestion, I tried that, and unfortunately it doesn't seem to have done anything. – ragavanmonke Jan 12 '23 at 18:40
  • if you attach the `.onAppear` to the `TabView` it works for me. – ChrisR Jan 12 '23 at 20:41
  • correction: works in preview but not in simulator !? – ChrisR Jan 12 '23 at 20:42
  • @ragavanmonke I have tested in `XCode14` with Simulator `14ProMax` its working for me with the change I have suggested. Make sure you have set the default value to 0 and then in onAppear set it to 1. If its still not working please let me know Xcode version and iOS version you are working on. – Nirav D Jan 13 '23 at 02:35
  • @NiravD I've added an edit to my original post with the suggestions and a recording from simulator (xcode 14.1, iphone 14) that shows it's not working. – ragavanmonke Jan 13 '23 at 03:25
  • Filed a bugreport to Apple. Having the same issue here: https://github.com/fl034/TabViewPageStyleBugTest . The marked answer didn't work for me. – heyfrank Apr 03 '23 at 15:11

2 Answers2

1

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.

alpennec
  • 1,864
  • 3
  • 18
  • 25
  • Doesn't work for me on iOS 16.1. Also this solution seems very dangerous since SwiftUI calls onAppear multiple times. Even if another tab is being shown. I think the behaviour originates from UIPageController what could being used under the hood, where always the next page was being preloaded. – heyfrank Apr 03 '23 at 14:59
0

This happened to me as well, when I was initializing the view with the third tab selected and then changing the selection binding to the first one.

See this minimal example: https://gist.github.com/fl034/970869befe8f8a3a4947ea97a6fb046d

Fixed it by introducing an extra property for a initial selected tab and a minimal delay. That way every view will be loaded and tapping

.onAppear {
    if let initiallySelectedTab {
        self.initiallySelectedTab = nil
        // Delay is needed to prevent SwiftUI bug where tapping the first tab wouldn't
        // change views. This way every view will be loaded.
        DispatchQueue.main.async {
            self.selectedTab = initiallySelectedTab
        }
    }
}

I also filed a bug at Apple (https://feedbackassistant.apple.com)

heyfrank
  • 5,291
  • 3
  • 32
  • 46