Search code examples
swiftswiftuialertswiftui-navigationlink

SwiftUI: pushViewController:animated: called on ... while an existing transition or presentation is occurring


I want to navigate to a detail view from within an alert.

Here a minimal reproducible code sample for the error:

struct ContentView: View {

    let items = ["1", "2", "3", "4"]

    var body: some View {
        NavigationView {
            List {
                ForEach(items, id: \.self) { item in
                    RowView(item: item)
                }
            }
        }
    }
}

struct RowView: View {

    let item: String

    @State private var isShowingNext = false
    @State private var isShowingAlert = false

    var body: some View {

        let itemNumber = Int(item)!
        let isEven = itemNumber % 2 == 0

        ZStack {
            if isEven {
                NavigationLink(
                    destination: Text("Hello"),
                    isActive: $isShowingNext,
                    label: {
                        Text(item)
                    }
                ).hidden()
                Button(
                    action: {
                        isShowingAlert.toggle()
                    }, label: {
                        Text("\(item) (with alert)")
                    }
                )
                .alert(isPresented: $isShowingAlert) {
                    Alert(
                        title: Text("Alert")
                            .foregroundColor(Color.red)
                            .font(.title),
                        message: Text("Hello"),
                        primaryButton: .destructive(
                            Text("Navigate"),
                            action: {
                                isShowingNext = true
                            }
                        ),
                        secondaryButton: .cancel()
                    )
                }
            } else {
                NavigationLink(
                    destination: Text("Hello"),
                    isActive: $isShowingNext,
                    label: {
                        Text(item)
                    }
                )
            }
        }
    }
}

When you click on e.g. the item 2 (with alert) the alert shows up but the error also appears in the console. Then when you click on Navigate inside the alert it doesn't navigate.

What could be the issue?


Solution

  • Both Alert and NavigationLink present another view. And you cannot present a view when the other view is already presented on current view.

    You have to wait until the Alert is hidden, and then you can present the NavigationLink.

    The easiest way to do this is to pass Binding to NavigationLink, which will make sure that the Alert is not displayed:

    struct RowView: View {
    
        let item: String
    
        @State private var isShowingNext = false
        @State private var isShowingAlert = false
    
        private var isShowingNextBinding: Binding<Bool> {
            Binding(
                get: {
                    !isShowingAlert && isShowingNext
                }, set: {
                    isShowingNext = $0
                }
            )
        }
    
        var body: some View {
    
            let itemNumber = Int(item)!
            let isEven = itemNumber % 2 == 0
    
            ZStack {
                if isEven {
                    NavigationLink(
                        destination: Text("Hello"),
                        isActive: isShowingNextBinding,
                        label: {
                            Text(item)
                        }
                    ).hidden()
                    Button(
                        action: {
                            isShowingAlert.toggle()
                        }, label: {
                        Text("\(item) (with alert)")
                    }
                    )
                        .alert(isPresented: $isShowingAlert) {
                            Alert(
                                title: Text("Alert")
                                    .foregroundColor(Color.red)
                                    .font(.title),
                                message: Text("Hello"),
                                primaryButton: .destructive(
                                    Text("Navigate"),
                                    action: {
                                        isShowingNext = true
                                    }
                                ),
                                secondaryButton: .cancel()
                            )
                        }
                } else {
                    NavigationLink(
                        destination: Text("Hello"),
                        isActive: $isShowingNext,
                        label: {
                            Text(item)
                        }
                    )
                }
            }
        }
    }