Skip to content

Instantly share code, notes, and snippets.

@henningko
Last active September 22, 2023 10:32
Show Gist options
  • Save henningko/586663db19aff5c80cd386a30b0af3e5 to your computer and use it in GitHub Desktop.
Save henningko/586663db19aff5c80cd386a30b0af3e5 to your computer and use it in GitHub Desktop.
SwiftData Context Crash
import Foundation
import SwiftData
import EventKit
@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 ?? ""
}
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
static func == (lhs: Event, rhs: Event) -> Bool {
return lhs.id == rhs.id
}
static func loadEvents(date: Date = Date()) -> [Event] {
let eventStore: EKEventStore = EKEventStore()
var events: [Event] = []
requestEventAccess(eventStore: eventStore) { granted in
if granted {
let endDate = date.addingTimeInterval(15 * 60) // 15 minutes from now
let startDate = date.addingTimeInterval(-24 * 60 * 60) // 24 hours ago
let predicate = eventStore.predicateForEvents(withStart: startDate, end: endDate, calendars: nil)
let calendarEvents = eventStore.events(matching: predicate)
// Filter the events to get ones that are either ongoing or have ended in the last 15 minutes.
let filteredEvents = calendarEvents.filter { calendarEvent in
if let eventEnd = calendarEvent.endDate {
let fifteenMinutesAgo = date.addingTimeInterval(-15 * 60)
return eventEnd > fifteenMinutesAgo
}
return false
}
events = filteredEvents.map { calendarEvent in
return Event(from: calendarEvent)
}
}
}
return events
}
static func requestEventAccess(eventStore: EKEventStore, completion: @escaping (Bool) -> Void) {
switch EKEventStore.authorizationStatus(for: .event) {
case .authorized:
completion(true)
case .denied, .restricted:
completion(false)
case .fullAccess:
completion(true)
case .writeOnly:
completion(false)
case .notDetermined:
eventStore.requestFullAccessToEvents { granted, error in
completion(granted)
}
@unknown default:
completion(false)
}
}
}
import Foundation
import SwiftData
@Model
class Note {
var id: UUID
var created: Date
var content: String
var location: Location?
var websites: [Website]?
@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
}
}
import SwiftUI
import SwiftData
struct NoteInputView: View {
@Environment(\.modelContext) private var context
var scrollProxy: ScrollViewProxy
@State private var newNoteContent = ""
@State private var showSelectLocationSheet: Bool = false
@State private var events: [Event] = []
var body: some View {
VStack {
HStack(spacing: 16) {
SelectEventButton(events: $events)
Button(action: {
self.showSelectLocationSheet = true
}, label: {
Label("Location", systemImage: "location")
.font(.caption)
})
.popover(isPresented: $showSelectLocationSheet) {
SelectLocationSheet()
}
}
.padding(.bottom)
HStack() {
TextField("Enter a quick note...", text: $newNoteContent, axis: .vertical)
.lineLimit(1...5)
.padding(.horizontal)
.padding(.vertical, 4)
.background(
RoundedRectangle(cornerRadius: 12)
.stroke(Color.gray.opacity(0.1), lineWidth: 1)
.background(
RoundedRectangle(cornerRadius: 12)
.fill(Color.background)
)
)
Button(action: {
let newNote: Note = addNote()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
withAnimation {
scrollProxy.scrollTo(newNote)
}
}
newNoteContent = ""
}) {
Image(systemName: "arrow.up.circle.fill")
.font(.title)
}
}
}
.padding([.horizontal, .vertical])
.overlay(
Rectangle()
.fill(Color.gray.opacity(0.1))
.frame(height: 1), alignment: .top
)
.background(Color.secondaryBackground)
}
func addNote() -> Note {
let selectedEvents = events.filter({ $0.isSelected })
// Option A: This crashes
let note = Note(newNoteContent, events: selectedEvents)
// Option B: This works
selectedEvents.forEach({context.insert($0)})
let note = Note(newNoteContent, events: events)
// Option C: This also works, despite notes also always referring to the same events
let note = Note(newNoteContent, events: Event.loadEvents())
context.insert(note) // <-- crash occurs here
do {
try context.save()
} catch {
print(error) // it is actually context.insert that crashes
}
return note
}
}
import SwiftUI
struct SelectEventButton: View {
@Binding var events: [Event]
@State var showSelectEventSheet: Bool = false
var body: some View {
Button(action: {
self.showSelectEventSheet = true
}, label: {
let selectedEvents = events.filter({ $0.isSelected })
if selectedEvents.count == 1 {
Label(selectedEvents.first!.name, systemImage: "calendar")
} else if selectedEvents.count > 1 {
Label("\(selectedEvents.count) Events", systemImage: "calendar")
} else {
Label("None", systemImage: "calendar")
}
})
.popover(isPresented: $showSelectEventSheet) {
SelectEventSheet(events: $events)
}
.onAppear {
loadEvents()
}
.onReceive(NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)) { _ in
loadEvents()
}
}
func loadEvents() {
events = Event.loadEvents()
let selectedEvents = events.filter({ $0.isSelected })
// By default, set to "most current" event
if events.count > 0 && selectedEvents.count == 0 {
events.first?.isSelected = true
}
}
}
import SwiftUI
import EventKit
struct SelectEventSheet: View {
@Binding var events: [Event]
@Environment(\.dismiss) private var dismiss
var body: some View {
NavigationView {
List(events) { event in
Button {
event.isSelected = !event.isSelected
} label: {
if event.isSelected {
Label(event.name, systemImage: "checkmark")
} else {
Label(event.name, systemImage: "")
}
}
.foregroundColor(.primary)
}
.navigationTitle("Select Events")
.toolbarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .confirmationAction) {
Button {
dismiss()
} label: {
Text("Done")
}
}
}
}
}
}
@henningko
Copy link
Author

Complete error:

*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Illegal attempt to establish a relationship 'events' between objects in different contexts (source = <NSManagedObject: 0x6000021f5bd0> (entity: Note; id: 0x600000351380 <x-coredata:///Note/t5CA3EA6C-936E-41A5-9A87-3B6A19564B9954>; data: {
    content = Dasfdsa;
    created = "2023-09-22 10:31:16 +0000";
    events =     (
    );
    id = "3D94A251-7E19-486E-8A0B-DDC97DF24978";
    location = nil;
    websites =     (
    );
}) , destination = <NSManagedObject: 0x6000021c8910> (entity: Event; id: 0xa94c1d99d92e51f3 <x-coredata://762DC494-B44D-4E7B-BC75-28D2616427F5/Event/p29>; data: {
    eventNotes = "";
    id = "25CD1E9A-7A2B-466B-9538-390317966DC9:63F539AB-5D5E-4A3A-ABAB-B18538846B19";
    isSelected = 1;
    name = Bla;
    notes =     (
    );
}))'

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment