SwiftUI Navigation in List View: Programmatically Dismiss Detail


Previously, we discussed how to programmatically navigate to a list item. In this article we will build upon that and look into how we can programmatically dismiss the detail view. This works with the current iOS version 13.4.1.


Pop Detail View on the iPhone


We will add a Dismiss button to our detail view, tapping on which will pop the detail view and set our selection to nil. To pop a view in SwiftUI we'll need to get the view's presentationMode from the Environment and call dismiss() on it's wrapped value.


struct DetailView: View {
    let item: Item
    @Binding var selectedItemId: UUID?
    
    @Environment(\.presentationMode) var presentationMode
    
    var body: some View {
        Text("Detail view for \(item.name).")
            .padding()
            .navigationBarItems(trailing:
                Button("Dismiss") {
                    self.presentationMode.wrappedValue.dismiss()
                    self.selectedItemId = nil
                }
            )
    }
}


This works well when we have a regular navigation view (horizontal size class is compact), but in split view calling dismiss() will do nothing.



Dismiss Detail View in Split View on the iPad


It's not possible to pop the detail view on the iPad. If we need to go back to a state where no item is selected, we will need to replace the detail view with some custom no selection view.


A common use case for dismissing the detail view on the iPad would be deleting the selected item. We can add a Delete button to our detail view, tapping on which will remove the selected item and set selection to nil.


struct DetailView: View {
    let item: Item
    @Binding var items: [Item]
    @Binding var selectedItemId: UUID?
    
    @Environment(\.presentationMode) var presentationMode
    
    var body: some View {
        Text("Detail view for \(item.name).")
            .padding()
            .navigationBarItems(trailing:
                Button("Delete") {
                    self.items.removeAll { $0.id == self.item.id }
                    self.selectedItemId = nil
                }
            )
    }
}


If we test this view on the iPad or a large iPhone in landscape, we can see that when we tap the Delete button, the item gets removed from the list, but the detail view is still showing the item that just got deleted.


To solve this we can add an if-statement to our detail view that will check if the selection is nil and display the NoSelectionView, otherwise display the normal item detail view content.


struct DetailView: View {
    let item: Item
    @Binding var items: [Item]
    @Binding var selectedItemId: UUID?
    
    @Environment(\.presentationMode) var presentationMode
    
    var body: some View {
        Group {
            if selectedItemId == nil {
                NoSelectionView()
            } else {
                Text("Detail view for \(item.name).")
                        .padding()
                .navigationBarItems(trailing:
                    Button("Delete") {
                        self.items.removeAll { $0.id == self.item.id }
                        self.selectedItemId = nil
                    }
                )
            }
        }
    }
}


Now our navigation split view almost works. The only problem is that the NoSelectionView gets the navigation bar items from the item detail view, even though we didn't set them on it.


This can be solved by explicitly setting the navigationBarItems of the NoSelectionView to an EmptyView.


struct NoSelectionView: View {
    
    var body: some View {
        Text("Nothing Selected")
        .navigationBarItems(trailing: EmptyView())
    }
}



The code for this article is available on GitHub.