Select the first item on a list and highlight it when the application launches

Viewed 92

When the app first launches in macOS (Big Sur), it populates a list with the items saved by the user. When the user clicks on an item on that list, a second view opens up displaying the contents of that item. Is there a way to select the first item on that list, as if the user clicked it, and display the second view when the app launches? Furthermore, if I delete an item on the list, I can't go back and select the first item on the list and displaying the second view for that item, or if I create new item, same applies, can't select it.

I have tried looking at answers here, like this, and this, and looked and tried code from a variety of places, but I can't get this to work.

So, using the code answered on my previous question, here's how the bare bones app looks like:

struct NoteItem: Codable, Hashable, Identifiable {
    let id: Int
    var text: String
    var date = Date()
    var dateText: String {
        dateFormatter.dateFormat = "EEEE, MMM d yyyy, h:mm a"
        return dateFormatter.string(from: date)
    }
    var tags: [String] = []
}

final class DataModel: ObservableObject {
    @AppStorage("notes") public var notes: [NoteItem] = []
}


struct AllNotes: View {
    
    @EnvironmentObject private var data: DataModel
    
    @State var noteText: String = ""
    
    var body: some View {
        NavigationView {
            List(data.notes) { note in
                NavigationLink(destination: NoteView(note: note)) {
                    VStack(alignment: .leading) {
                        Text(note.text.components(separatedBy: NSCharacterSet.newlines).first!)
                        Text(note.dateText).font(.body).fontWeight(.light)
                    }
                    .padding(.vertical, 8)
                }
            }
            .listStyle(InsetListStyle())
            
            Text("Select a note...")
                .frame(maxWidth: .infinity, maxHeight: .infinity)
        }
        .navigationTitle("A title")
        .toolbar {
            ToolbarItem(placement: .navigation) {
                Button(action: {
                    data.notes.append(NoteItem(id: UUID(), text: "New Note", date: Date(), tags: []))
                }) {
                    Image(systemName: "square.and.pencil")
                }
            }
        }
    }
}

struct NoteView: View {
    
    @EnvironmentObject private var data: DataModel
    var note: NoteItem
    
    @State var text: String = ""
    var body: some View {
        HStack {
            VStack(alignment: .leading) {
                TextEditor(text: $text).padding().font(.body)
                    .onChange(of: text, perform: { value in
                        guard let index =     data.notes.firstIndex(of: note) else { return }
                        data.notes[index].text = value
                    })
                Spacer()
            }
            Spacer()
        }
        .padding()
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .background(Color.white)
        .onAppear() {
            print(data.notes.count)
        }
    }
}

I have tried adding @State var selection: Int? in AllNotes and then changing the list to

List(data.notes, selection: $selection)

and trying with that, but I can't get it to select anything. Sorry, newbie here on SwiftUI and trying to learn.

Thank you!

1 Answers

You were close. Table view with selection is more about selecting item inside table view, but you need to select NavigationLink to be opened

There's an other initializer to which does exactly what you need. To selection you pass current selected item. To tag you pass current list item, if it's the same as selection, NavigationLink will open

Also you need to store selectedNoteId instead of selectedNote, because this value wouldn't change after your update note properties

Here I'm setting selectedNoteId to first item in onAppear. You had to use DispatchQueue.main.async hack here, probably a NavigationLink bug

To track items when they get removed you can use onChange modifier, this will be called each time passed value is not the same as in previous render

struct AllNotes: View {
    
    @EnvironmentObject private var data: DataModel
    
    @State var noteText: String = ""
    @State var selectedNoteId: UUID?
    
    var body: some View {
        NavigationView {
            List(data.notes) { note in
                NavigationLink(
                    destination: NoteView(note: note),
                    tag: note.id,
                    selection: $selectedNoteId
                ) {
                    VStack(alignment: .leading) {
                        Text(note.text.components(separatedBy: NSCharacterSet.newlines).first!)
                        Text(note.dateText).font(.body).fontWeight(.light)
                    }
                    .padding(.vertical, 8)
                }
            }
            .listStyle(InsetListStyle())
        }
        .navigationTitle("A title")
        .toolbar {
            ToolbarItem(placement: .navigation) {
                Button(action: {
                    data.notes.append(NoteItem(id: UUID(), text: "New Note", date: Date(), tags: []))
                }) {
                    Image(systemName: "square.and.pencil")
                }
            }
        }
        .onAppear {
            DispatchQueue.main.async {
                selectedNoteId = data.notes.first?.id
            }
        }
        .onChange(of: data.notes) { notes in
            if selectedNoteId == nil || !notes.contains(where: { $0.id == selectedNoteId }) {
                selectedNoteId = data.notes.first?.id

            }
        }
    }
}

Not sure what's with @AppStorage("notes"), it shouldn't work because this annotation only applied to simple types. If you wanna store your items in user defaults you had to do it by hand.

After removing it, you were missing @Published, that's why it wasn't updating in my case. If AppStorage could work, it may work without @Published

final class DataModel: ObservableObject {
    
    @Published
    public var notes: [NoteItem] = [
        NoteItem(id: UUID(), text: "New Note", date: Date(), tags: []),
        NoteItem(id: UUID(), text: "New Note", date: Date(), tags: []),
        NoteItem(id: UUID(), text: "New Note", date: Date(), tags: []),
        NoteItem(id: UUID(), text: "New Note", date: Date(), tags: []),
    ]
}
Related