I have a Model Class Note:
@Model
class Note {
var id: UUID
var created: Date
var content: String
@Relationship(inverse: \Event.notes)
var events: [Event]?
init(_ content: String, created: Date = .now, events: [Event] = []) {
self.id = UUID()
self.created = created
self.content = content
self.events = events
}
}
And Event:
@Model
class Event: Hashable, Equatable {
var id: String
var name: String
var eventNotes: String?
@Relationship var notes: [Note]?
// @Transient does not publish (iOS bug?), use .ephemeral instead
@Attribute(.ephemeral) var isSelected: Bool = false
init(_ name: String = "Unnamed Event", calendarId: String, eventNotes: String) {
self.id = calendarId
self.name = name
self.eventNotes = eventNotes
}
init(from calendarEvent: EKEvent) {
self.id = calendarEvent.eventIdentifier
self.name = calendarEvent.title
self.eventNotes = calendarEvent.notes ?? ""
}
...
static func loadEvents(date: Date = Date()) -> [Event] {
...
}
}
I have the following View hierarchy
NoteInputView which has @State var events: [Event] = []
SelectEventButton which has @Binding var events: [Event] and calls Event.loadEvent() to retrieve list of events
SelectEventSheet which has @Binding var events: [Event] and lets the user toggle isSelected
GitHub Gist with all relevant files
Adding notes with same events crashes...
With this setup, I attempt so save new notes in NoteInputView by calling addNote:
func addNote() -> Note {
let selectedEvents = events.filter({ $0.isSelected })
let note = Note(newNoteContent, events: selectedEvents)
context.insert(note)
do {
try context.save()
} catch {
print(error)
}
return note
}
This works for the first note after opening the app, or if every subsequent note has a different event selected. However, storing a second note with the same event crashes with the following error:
"Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Illegal attempt to establish a relationship 'events' between objects in different contexts"
(complete error see here) The error occurs at context.insert, which doesn't throw.
If I force quit the app, and then add a note with the same events as an already persisted note, no error is thrown (until a I add another note with the same event without force-quitting).
... but not because one cannot refer to the same events twice
It's not a problem of referring to the same events, as the following code also works fine for multiple notes:
func addNote() -> Note {
// This works, despite notes also always referring to the same events
let note = Note(newNoteContent, events: Event.loadEvents())
context.insert(note)
do {
try context.save()
} catch {
print(error)
}
return note
}
.
... workaround? Manually adding events to the context before adding it to the notes
One workaround seems to be to add the events to the context before adding the note:
func addNote() -> Note {
let selectedEvents = events.filter({ $0.isSelected })
selectedEvents.forEach({context.insert($0)})
let note = Note(newNoteContent, events: events)
context.insert(note)
do {
try context.save()
} catch {
print(error)
}
return note
}
.
... but why?
While this works, I cannot quite make sense of this. It seems that passing events around between views may be the culprit, or that loadEvents is called in a child view.
Would love some advice, since this doesn't seem like intended behavior.
Post
Replies
Boosts
Views
Activity
I have tried updating my app form NavigationView to NavigationSplitView.
Previously, my code looked like this:
// ContentView.swift
import SwiftUI
import SwiftData
struct ContentView: View {
@Environment(\.modelContext) private var context
@Query(sort: \Note.created, order: .reverse) var notes: [Note]
var body: some View {
NavigationView {
ScrollViewReader { scrollProxy in
ZStack() {
NoteListView(notes: notes)
NoteInputView(scrollProxy: scrollProxy)
.frame(maxHeight: .infinity, alignment: .bottom)
}
}
.navigationBarTitle(Text("Notes"))
}
}
}
The reason I am using a ZStack is because the NoteInputView has a keyboard that should be dismissible on tap. However, there is a bug that prevents swipeActions from receiving a tap if onTapGesture is used elsewhere, see description here:
https://stackoverflow.com/questions/77162726/swiftui-swipeactions-not-working-when-tap-gesture-is-used-elsewhere-focusstate
When using NavigationView, this behaves just fine:
Scrolled down:
Scrolled to top:
Using NavigationSplitView, my code looks like this:
// ContentView.swift
import SwiftUI
import SwiftData
struct ContentView: View {
@Environment(\.modelContext) private var context
@Query(sort: \Note.created, order: .reverse) var notes: [Note]
@State var selection: Set<Note> = []
var body: some View {
ScrollViewReader { scrollProxy in
NavigationSplitView {
ZStack {
NoteListView(notes: notes, selection: $selection)
NoteInputView(scrollProxy: scrollProxy)
.frame(maxHeight: .infinity, alignment: .bottom)
}
.navigationTitle(Text("Notes"))
} detail: {
if let noteSelected = selection.first {
NoteView(note: noteSelected)
} else {
Text("No note selected")
.foregroundStyle(Color.secondary)
}
}
}
}
}
Where now the toolbar/title doesn't scroll appropriately:
Removing ZStack and putting the NoteInputView into an .overlay results in the same behavior. When I remove the ZStack and my NoteInput, the scrolling also works as desired. Wrapping the ZStack into a ScrollView hides the notes.
Seems to me that this might be a bug, or does anyone have another workaround in mind?