SwiftUI Navigation in List View: Default Selection


In this article we will discuss how to programmatically pre-select an item in List view, when the view appears. This can be useful in split view on iPad when we don't want to show an empty welcome detail view on the right-hand side. It can also be used for state restoration on iPhone and iPad, so that when the user launches the app, they can see the item that they were previously viewing. This article is relevant for the current iOS version 13.4.1.


We are going to start with the code from our previous article on Programmatic Navigation. You can get the starting code from here, and the completed code for this article from here.


We will first look at some ways that we might implement the default selection, but which will cause a broken behaviour. Then we will try the solution that works. If you just want a quick tip, go straight to the section with the Working Solution title.


For demo purposes we will change our Item's id property to be of type Int. Then when creating the initial list of items we'll provide each item an id.


struct Item: Identifiable {
    let id: Int
    let name: String
}

struct ContentView: View {
    @State private var items = [
        Item(id: 1, name: "Item 1"),
        Item(id: 2, name: "Item 2"),
        Item(id: 3, name: "Item 3")
    ]
    
    ...
}



Solutions with Problems


After some experimentation, it looks like our programmatic navigation will break if we set the selection before the view appears on screen.


Assign Default Value to Selection Property


Let's imagine that we know what item we want to select when the app launches. Then we might want to assign that id as the default value to the selectedItemId property.


@State private var selectedItemId: Int? = 1


But this will cause our navigation to behave in a strange way. On a narrow screen on iPhone, it will navigate to the correct item, but the back button will be broken. In split view on iPad it will show the detail view in place of the list view, and there will be no way to view the list.


Set Selection in the View Initializer


We can try to set the selection in the initializer. The selected id might be passed in as an argument or we might have some logic for selection, for example, we want to select the first item in the list.


init(selectedId: Int? = nil) {
    if let selectedId = selectedId {
        self.selectedItemId = selectedId
    } else {
        self.selectedItemId = self.items.first?.id
    }
}


But this will have absolutely no effect on the navigation. The selection set in the initializer will be ignored.



Working Solution


For the default selection to work properly with our programmatic navigation, we have to set it after the view has appeared on screen. We can do it inside the onAppear(perform:) modifier.


NavigationView {
}
.onAppear {
    self.selectedItemId = self.items.first?.id
}



Default Selection Passed in the Initializer


If the selected id is passed to the view in the initializer, then we'll need to store it in a property and assign it to selection inside the onAppear.


struct ContentView: View {
    @State private var items = [
        Item(id: 1, name: "Item 1"), Item(id: 2, name: "Item 2")
    ]
    
    @State private var selectedItemId: Int?

    private let initialSelection: Int?
    
    init(selectedId: Int? = nil) {
        self.initialSelection = selectedId
    }
    
    var body: some View {
        NavigationView {
            ...
        }
        .onAppear {
            if let initialSelection = self.initialSelection {
                self.selectedItemId = initialSelection
            } else {
                self.selectedItemId = self.items.first?.id
            }
        }
    }
}



Default Selection Only in Split View


If you only want to set the default selection when in split view, then you can check if the horizontal size class is regular.


struct ContentView: View {

    @Environment(\.horizontalSizeClass) var horizontalSizeClass
    ...
    
    var body: some View {
        NavigationView {
            ...
        }
        .onAppear {
            if self.horizontalSizeClass == .regular {
                // set default selection
            }
        }
    }
}



Don't hesitate to take the code for this article from our GitHub.