swiftswiftuiforeach

Possible to have two different kinds of objects in a SwiftUI selectable List?


in a book club app I'm trying to write, I have a NavigationSplitView which displays a list of readers in a selectable List that gives the user access to each reader's comments on a given book. At the top of the list is a button "Book" that takes the user back to the book's main page:

struct ProjectView: View
{
    @State private var readers: [Reader]
    @State private var selectedReader: Reader?
    
    init(readers: [Reader])
    {
        self.readers = readers
    }
    
    var body: some View
    {
        NavigationSplitView
        {
                List(selection: self.$selectedReader)
                {
                    Button("Book")
                    {
                        self.selectedReader = nil
                    }
                    
                    Divider()
                                        
                    ForEach (self.readers, id: \.self)
                    {
                        reader in Text(reader.name)
                    }
                }
        }
        detail:
        {
            if let selectedReader = self.selectedReader
            {
                ReaderView(reader: selectedReader)
            }
            else
            {
                BookView()
            }
        }
        #if os(macOS)
        .navigationSplitViewColumnWidth(min: 180, ideal: 200)
        #endif
        .navigationTitle(self.project.name)
    }
}

I'm trying to make it so that "Book" will select / highlight just as the reader names in the reader list do, you can see what I mean in this video:

https://youtube.com/shorts/__IA890FxG0?feature=share

Does anyone know how to accomplish this? Or is it even possible?


Solution

  • You should use a ReaderOrBook?, since the selection can be either a reader, or "Book", or nothing is selected at all (nil).

    enum ReaderOrBook: Hashable {
        case book
        case reader(Reader)
        
        // convenient property to convert a ReaderOrBook to a Reader 
        var asReader: Reader? {
            if case let .reader(r) = self {
                return r
            }
            return nil
        }
    }
    

    Then you just need to tag the list rows appropriately.

    struct ProjectView: View {
        // this should not be a @State since you want callers to pass in an array of readers
        // if you want to mutate this in ProjectView, make it a @Binding instead
        let readers: [Reader]
        @State private var selectedReaderOrBook: ReaderOrBook?
        
        var body: some View {
            NavigationSplitView {
                List(selection: self.$selectedReaderOrBook) {
                    Text("Book").tag(ReaderOrBook.book)
                    
                    Divider()
                    
                    ForEach (self.readers, id: \.self) { reader in
                        Text(reader.name)
                            .tag(ReaderOrBook.reader(reader))
                    }
                }
            } detail: {
                // ...
            }
        }
    }