I'm playing around with MVVM and have trouble wrapping my head around how to work with selecting a single element.
Following online examples I've written something basic that fetches a list of data and displays it.
struct NoteListView: View {
@State private var model = NoteModel()
var body: some View {
NavigationStack {
List(model.notes) { note in
NavigationLink {
NoteDetailView(note: note)
} label: {
Text(note.title)
}
}
.task {
model.fetchNotes()
}
}
}
}
struct NoteDetailView: View {
let note: Note
var body: some View {
Text(note.title)
.font(.title)
Text(note.content)
}
}
@Observable
class NoteModel {
var notes: [Note] = []
func fetchNotes() {
self.notes = [
Note(title: "First note", content: "Test note"),
Note(title: "Reminder", content: "Don't forget to water the plants!"),
Note(title: "Shopping list", content: "Eggs, milk, hat, bread")
]
}
}
struct Note: Identifiable, Hashable {
let id = UUID()
var title: String
var content: String
}
Now I want to update a note on the detail view. Updating involves a PUT request to the server with the server calculating data for the update, so after the request is successful the new version of the note needs to be fetched. I can't seem to figure out how to write this. I think the model would look something like this.
@Observable
class NoteModel {
var notes: [Note] = []
var selectedNote: Note?
func fetchNotes() {
self.notes = [
Note(title: "First note", content: "Test note"),
Note(title: "Reminder", content: "Don't forget to water the plants!"),
Note(title: "Shopping list", content: "Eggs, milk, hat, bread")
]
}
func fetchNote(title: String) {
// fetch a single note, probably used after updating a note to display the updated note on the details view
self.selectedNote = APIClient.fetchNote()
}
func updateNote(title: String, content: String) {
self.selectedNote?.title = title
self.selectedNote?.content = content
// makes a PUT request to update the note, after which the note needs to be fetched
}
}
But I don't know how to pass the selected note to the details view and how to update the note displayed on the details view and on the list view. I imagine that this is a fairly basic scenario for MVVM, but I couldn't find any examples illustrating the basic conventions on how to do this.
Update
Based on the answer from @jiseong-lim I've changed my code a bit and gotten a working version. In the below code the Update button changes the title and text of the note on the detail view. Navigating back resets the notes list because the .task {} modifier triggers again, 'fetching' a hardcoded list.
This works, but it feels clunky to have to pass a specific note and the model.
struct NoteListView: View {
@State private var model = NoteModel()
var body: some View {
NavigationStack {
List($model.notes) { $note in
NavigationLink {
NoteDetailView(note: $note, model: model)
} label: {
Text(note.title)
}
}
.task {
model.fetchNotes()
}
}
}
}
struct NoteDetailView: View {
@Binding var note: Note
var model: NoteModel
var body: some View {
Text(note.title)
.font(.title)
Text(note.content)
Button("Update") {
let updatedNote = Note(id: note.id, title: "\(note.title) (updated)", content: "Updated text!")
model.updateNote(note: updatedNote)
}
}
}
@Observable
class NoteModel {
var notes: [Note] = []
var selectedNote: Note?
func fetchNotes() {
print("Fetching notes...")
self.notes = [
Note(title: "First note", content: "Test note"),
Note(title: "Reminder", content: "Don't forget to water the plants!"),
Note(title: "Shopping list", content: "Eggs, milk, hat, bread")
]
}
func updateNote(note: Note) {
if let index = notes.firstIndex(where: { $0.id == note.id }) {
notes[index] = note
}
}
}
struct Note: Identifiable, Hashable {
let id: UUID
var title: String
var content: String
init(id: UUID = UUID(), title: String, content: String) {
self.id = id
self.title = title
self.content = content
}
}
PUT request to the server..., or how to pass the model to theNoteDetailView? In the latter case use@Environment(NoteModel.self) var model, from this you get access to all its functions and theselectedNotein yourNoteDetailView. See also Apple official documentation Managing model data in your app for how to use and pass@Observable classmodels.selectedNoteof the model when a user taps aNavigationLinkand how I can update theselectedNoteandnotesfields of the model when a user updates a note through the details view.@Bindable var book = bookin the List for a selected item. I've tried just assigning it to the variable of the model,model.selectedNote = note, as that is an observable field but that doesn't work.