Search code examples
swiftuiswiftui-tabview

Showing alert from a tab in "More" overflow sends me back to the previous view


I have what should be a simple operation, show an alert in a SwiftUI app that uses a TabView. However, whenever I first show an alert from a tab that was pushed to the "More" overflow screen it immediately takes me back to whatever "main tab" (non-More) I was on before.

This only happens the first time I show the alert, after that it works as expected. However, on all subsequent alerts (when it is behaving correctly) I get this output in the console

2021-01-07 12:19:49.289546-0700 Test App[48718:25806605] setViewControllers:animated: called on <UIMoreNavigationController 0x7fcecf826c00> while an existing transition or presentation is occurring; the navigation stack will not be updated.

If the "Button" tab is one of the primary ones (not on the "More" screen) this doesn't happen.

This same behavior also happens with the In-App purchase popup from StoreKit, not just an Alert I launch.

How can I fix this? Is this a TabView bug?

Running Xcode 12.3

Replicated in the simulator on: iPhone 11, iPhone 12, iPad Pro

Steps to replicate (using the code below):

  • Launch the app
  • Go to More -> Button Tab
  • Click "show alert"
  • Alert is shown but view behind it immediately returned to Tab One
  • Repeat the same steps and it doesn't happen again (have to re-launch the app)

Animation of the issue

import SwiftUI

struct ContentView: View {
    let textTabs = ["One","Two","Three","Four","Five","Six","Seven","Eight"]
    
    var body: some View {
        TabView {
            ForEach(textTabs, id: \.self) { name in
                Text(name).tabItem {
                    Image(systemName: "circle")
                    Text(name)
                }
            }
            
            ButtonTab().tabItem {
                Image(systemName: "face.smiling")
                Text("Button")
            }
        }
    }
}

struct ButtonTab: View {
    @State var showAlert = false
    
    var body : some View {
        VStack {
            Button("show alert") {
                showAlert = true
            }
        }
        .alert(isPresented: $showAlert) {
            Alert(title: Text("hello"),
                  dismissButton: .default(Text("ok")))
        }
    }
}

Solution

  • A possible workaround is to maintain the selection on Button click:

    struct ContentView: View {
        let textTabs = ["One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight"]
    
        @State private var selection = "Two" // store selection as a `@State`
        
        var body: some View {
            TabView(selection: $selection) {
                ForEach(textTabs, id: \.self) { name in
                    Text(name)
                        .tabItem {
                            Image(systemName: "circle")
                            Text(name)
                        }
                }
    
                ButtonTab(selection: $selection)
                    .tabItem {
                        Image(systemName: "face.smiling")
                        Text("Button")
                    }
                    .tag("Button") // add tag for `selection`
            }
        }
    }
    
    struct ButtonTab: View {
        @State private var showAlert = false
        @Binding var selection: String // pass selection as a `@Binding`
    
        var body: some View {
            VStack {
                Button("show alert") {
                    showAlert = true
                    selection = "Button" // (force) set `selection` here
                }
            }
            .alert(isPresented: $showAlert) {
                Alert(title: Text("hello"),
                      dismissButton: .default(Text("ok")))
            }
        }
    }