Search code examples
swiftswiftuiswiftui-list

Programmatically select item from list


I have a list with NavigationLinks in it. I wish to programmatically select/tap an item from the list. I tried using NavigationLink(tag..., selection...) API, but looks like it's deprecated from iOS 16.

Code:

import SwiftUI
import Foundation

struct BookItem {
    let id: Int
    let name: String
    
    init(id: Int, name: String) {
        self.id = id
        self.name = name
    }
}

struct ContentView: View {
    let bookList: [BookItem] = [BookItem.init(id: 0, name: "History"), BookItem.init(id: 1, name: "Math"), BookItem.init(id: 2, name: "Science")]

    let selectBookItemId: Int = 1
    
    var body: some View {
        NavigationStack {
            VStack {
                List(bookList, id: \.name) { bookItem in
                    NavigationLink(destination: { DetailsView(bookItem: bookItem) },
                    label: {
                        HStack {
                            Text(bookItem.name)
                            Spacer()
                        }
                        .contentShape(Rectangle())
                    })
     
                    // NOTE: Was going to use this, but it is deprecated
//                    NavigationLink(
//                        destination: ,
//                        tag:,
//                        selection:,
//                        label: {})
                }
            }
        }
    }
}

struct DetailsView: View {
    var bookItem: BookItem

    var body: some View {
        VStack {
            Text(bookItem.name).font(.body.bold()).foregroundColor(Color.blue)
        }
    }
}

In my sample code above, let's say I wish to programmatically select 2nd item from the list i.e. when I execute the code, it should show me DetailsView with "Math" written on it.

How can it be achieved now since NavigationLink API is deprecated?

Thanks!

Edit: With @son's answer, I was able to solve my original question. I thought it will work with my actual app data, but my data is more complex. I need to pass an array objects to destination view and when I try to do that I am getting an error.

Code:

import SwiftUI
import Foundation

struct BookItem: Hashable {
    let id: Int
    let name: String
    let listing: [String]
    
    init(id: Int, name: String, listing: [String]) {
        self.id = id
        self.name = name
        self.listing = listing
    }
}

struct ContentView: View {
    let bookList: [BookItem] = [BookItem.init(id: 0, name: "History", listing: ["1", "2"]),
                                BookItem.init(id: 1, name: "Math", listing: ["1", "2"]),
                                BookItem.init(id: 2, name: "Science", listing: ["1", "2"])]

    let selectBookItemId: Int
    @State private var path = NavigationPath()
    
    var body: some View {
        NavigationStack(path: $path) {
            VStack {
                List(bookList, id: \.name) { bookItem in
                    Button {
                        path.append(bookItem.listing)
                    } label: {
                        HStack {
                            Text(bookItem.name)
                            Spacer()
                        }
                        .contentShape(Rectangle())
                    }
                }
            }
            .navigationDestination(for: String.self, destination: { listing in
                DetailsView(bookItem: listing)
            })
            .onAppear { //<- append book at index here
                if let book = bookList.first(where: {$0.id == selectBookItemId}) {
                    path.append(book.listing)
                }
            }
        }
    }
}

struct DetailsView: View {
    var bookItem: [String]

    var body: some View {
        VStack {
           // Text(bookItem.name).font(.body.bold()).foregroundColor(Color.blue)
        }
    }
}

Error: Cannot convert value of type 'String' to expected argument type '[String]'

Solution

  • An alternative way is to use NavigationPath. You could try this:

    @State private var path = NavigationPath()
    
    let selectBookItemId: Int
    
    NavigationStack(path: $path) {
        VStack { 
            List(bookList, id: \.name) { bookItem in
                Button {
                    path.append(bookItem)
                } label: {
                    HStack {
                        Text(bookItem.name)
                        Spacer()
                    }
                    .contentShape(Rectangle())
                }
            }
        }
        .navigationDestination(for: BookItem.self, destination: { book in
            DetailsView(bookItem: book)
        })
        .onAppear { //<- here
            if let book = bookList.first(where: {$0.id == selectBookItemId}) {
                path.append(book)
            }
        }
    }
    

    And the BookItem must conform Hashable

    struct BookItem: Hashable {
        ...
    }
    

    And passing id in such as:

    ContentView(selectBookItemId: 1)