1

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!

Aleph
  • 465
  • 2
  • 12

1 Answers1

1

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: []),
    ]
}
Phil Dukhov
  • 67,741
  • 15
  • 184
  • 220
  • Oh, neat, thanks! Only issue I need to solve is that on `NoteView`, where the TextEditor sits, I have an `.onChange` that tries to update the text on the `NavigationLink`, like: `data.notes[index].text = value` and now your solution, is causing each character typed on the texteditor to bring back the focus to the NavigationLink.... Let me figure out how I get our of this loop: app selects a navigation link, user types on the text editor to add text, view passes new text to the navigation link, which tries to update the text and get focus to it at the same time... – Aleph Aug 17 '21 at 18:06
  • In terms of the AppStorage, originally I was using a different approach, but the solution I got on the other question suggested I use this, which is working for me. – Aleph Aug 17 '21 at 18:08
  • I had it as final class DataModel: ObservableObject { @Published("notes") public var items: [NoteItem] = [] } – Aleph Aug 17 '21 at 18:44
  • @Aleph Yes, this happens because when you're storing `selectedNote` and than change note in data model, these are not equal anymore. You need to store `selectedNoteId` instead of `selectedNote`, I've updated my answer – Phil Dukhov Aug 18 '21 at 04:16
  • That makes sense, but it is still doing it. I'm going to build a new project and only add my original code, plus your solution and see if there any something else causing this. – Aleph Aug 18 '21 at 09:09
  • Oh, I see what's going on. The selection is jumping to the top all the time. It goes to the first item in the list. I don't know why, maybe it triggesr that's the `.onAppear` all the time? I don't quite understand SwuiftUI yet, but that's my gut telling me. – Aleph Aug 18 '21 at 09:13
  • Or maybe the code `if selectedNoteId == nil || !notes.contains(where: { $0.id == selectedNoteId }) { selectedNoteId = data.notes.first?.id` in onChange? – Aleph Aug 18 '21 at 09:13
  • So, I commented `.id(data.notes)` and it stopped doing it... Removing it causes new items not to get focused, but maybe there is another way to bring the focus to them... – Aleph Aug 18 '21 at 09:17
  • @Aleph where's `.id(data.notes)` came from? I can't see it in your sample code, and it's not in presented in my answer – Phil Dukhov Aug 18 '21 at 09:18
  • Your code. Look at the edit you made, it's there in red (removed). If you removed it with the new answer, then it's my bad that I left it, but now I can't quite select the new item, – Aleph Aug 18 '21 at 09:22
  • Fixed it by adding `selectedNoteId = data.notes.first?.id` after `data.notes.append` – Aleph Aug 18 '21 at 09:25
  • @Aleph I see, yes I've removed it, because you've asked this code to work when you edit text too, so it's not needed anymore =) forget about selecting new item, good job figuring it out on your own – Phil Dukhov Aug 18 '21 at 09:29