Search code examples
swiftswiftuiswiftdata

What is the effect of the insert order in SwiftData for dependent and parent instances?


In SwiftData, what is the order of insertion for dependent and parent instances? In the snippet below, it works when the dependent instance is inserted before the parent. However, it fails when the order is reversed. Why is this the case?


import Foundation
import SwiftData
import SwiftUI

struct DebugView: View {
  @Query var parent: [ParentClass]
  @Query var dept: [DependentClass]
  var body: some View {
    Text("parent cnt:\(parent.count), dept cnt:\(dept.count)")
  }
}

@Model
class ParentClass {
  @Relationship(deleteRule: .cascade)
  var deps: DependentClass

  init(deps: DependentClass) {
    self.deps = deps
  }
}

@Model
class DependentClass {
  var id: String
  init(id: String = UUID().uuidString) {
    self.id = id
  }
}

#Preview {
  do {
    let config = ModelConfiguration(isStoredInMemoryOnly: true)
    let container = try ModelContainer(for: ParentClass.self, configurations: config)
    let context = ModelContext(container)

    var dept = DependentClass()
    var parentClass = ParentClass(deps: dept)

    /* ok to insert dept before parent. View shows: "parent cnt:1, dept cnt:1" */
    context.insert(dept) 
    context.insert(parentClass)

    /* ok to insert only parent, swiftData will insert dept too. View shows: "parent cnt:1, dept cnt:1" */
    // context.insert(parentClass) 

    /* failed to insert dept after parent, error list below */
    // context.insert(parentClass)
    // context.insert(dept) 

    return DebugView()
      .modelContainer(container)
  } catch {
    return Text("Failed to create container: \(error.localizedDescription)")
  }
}

The error log insert dept after parent:

Date/Time:           2024-01-13 14:10:10.3694 +0800
Launch Time:         2024-01-13 14:10:09.9932 +0800
OS Version:          macOS 14.1.1 (23B81)
Release Type:        User
Report Version:      104

Exception Type:  EXC_BREAKPOINT (SIGTRAP)
Exception Codes: 0x0000000000000001, 0x00000001929f3938
Termination Reason: SIGNAL 5 Trace/BPT trap: 5
Terminating Process: exc handler [51868]

Triggered by Thread:  0

Thread 0 Crashed::  Dispatch queue: com.apple.main-thread
0   libswiftCore.dylib                     0x1929f3938 _assertionFailure(_:_:file:line:flags:) + 248
1   SwiftData                              0x1c42fc454 0x1c42d1000 + 177236
2   SwiftData                              0x1c4315aac 0x1c42d1000 + 281260
3   SwiftData                              0x1c42fd4e0 0x1c42d1000 + 181472
4   DebugView.1.preview-thunk.dylib        0x10161ae10 closure #1 in static $s39gogodict_PreviewReplacement_DebugView_133_20B25209EACDFA98A88DFB5B90B26E4CLl0B0fMf_15PreviewRegistryfMu_.makePreview() + 840 (@__swiftmacro_39gogodict_PreviewReplacement_DebugView_133_20B25209EACDFA98A88DFB5B90B26E4CLl0B0fMf_.swift:18)
5   PreviewsInjection                      0x1d6aaa0ec 0x1d6a6f000 + 241900
6   PreviewsInjection                      0x1d6aab060 0x1d6a6f000 + 245856
7   libswift_Concurrency.dylib             0x1e450b738 static MainActor.assumeIsolated<A>(_:file:line:) + 144
8   PreviewsInjection                      0x1d6aa9e48 0x1d6a6f000 + 241224
9   PreviewsInjection                      0x1d6aadfb8 0x1d6a6f000 + 257976
10  PreviewsInjection                      0x1d6aaea88 0x1d6a6f000 + 260744
11  PreviewsInjection                      0x1d6a9c89c 0x1d6a6f000 + 186524
12  PreviewsInjection                      0x1d6a9f5c8 0x1d6a6f000 + 198088
13  PreviewsInjection                      0x1d6a87c90 0x1d6a6f000 + 101520
14  PreviewsInjection                      0x1d6a88128 0x1d6a6f000 + 102696
15  PreviewsInjection                      0x1d6aa0fa0 0x1d6a6f000 + 204704
16  PreviewsInjection                      0x1d6a777b8 0x1d6a6f000 + 34744
17  PreviewsInjection                      0x1d6a77004 0x1d6a6f000 + 32772
18  PreviewsFoundation                     0x1d69b4630 0x1d691c000 + 624176
19  libdispatch.dylib                      0x18016b4f4 _dispatch_call_block_and_release + 24
20  libdispatch.dylib                      0x18016cd3c _dispatch_client_callout + 16
21  libdispatch.dylib                      0x18017bb24 _dispatch_main_queue_drain + 1272
22  libdispatch.dylib                      0x18017b61c _dispatch_main_queue_callback_4CF + 40
23  CoreFoundation                         0x1803f1a30 __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ + 12
24  CoreFoundation                         0x1803ec148 __CFRunLoopRun + 1936
25  CoreFoundation                         0x1803eb5a4 CFRunLoopRunSpecific + 572
26  GraphicsServices                       0x18e9fbae4 GSEventRunModal + 160
27  UIKitCore                              0x1852f02e4 -[UIApplication _run] + 868
28  UIKitCore                              0x1852f3f5c UIApplicationMain + 124
29  SwiftUI                                0x1c51fc1b0 0x1c4371000 + 15249840
30  SwiftUI                                0x1c51fc050 0x1c4371000 + 15249488
31  SwiftUI                                0x1c4f02fa4 0x1c4371000 + 12132260
32  gogodict                               0x1009f03b0 static GOGODictApp.$main() + 40
33  gogodict                               0x1009f0474 main + 12 (GOGODictApp.swift:12)
34  dyld_sim                               0x100bfd544 start_sim + 20
35  dyld                                   0x100ce60e0 start + 2360

It seems perplexing to someone new like me. Is there any documentation or article that explains the underlying logic of the engine?

Update: Thanks to @Joakim, offer an opinion: what caused the crash here is that an object was inserted more than once into the model context.

However, I still confuse: It appears that there is no crash insert same instance twice. Like below, I insert both dependent and parent twice successfully. Only crash when parent first and dependent follow.

    /* ok, View shows: "parent cnt:1, dept cnt:1" */
    context.insert(parentClass)
    context.insert(parentClass)
    
    /* ok, View shows: "parent cnt:0, dept cnt:1" */
    //context.insert(dept)
    //context.insert(dept)

    /* crash */
    //context.insert(parentClass)
    //context.insert(dept) 

What is different:

  1. insert two same instance directly (ok)
  2. insert two same instance by parent first and then directly (crash)

Solution

  • When you assign a relationship property for an object then SwiftData will handled the insert into the current ModelContext instance for you if needed.

    So when you have an init like the one for ParentClass

    init(deps: DependentClass) {
      self.deps = deps
    }
    

    Then DependentClass object (from here on I refer to them as parent and dependent) will be inserted into the same context as parent if it has been inserted.

    The problem is that you can (for now) never insert the same object, if it has a relationship property set, twice into the same context or you will get a crash (for objects with no relationships or if they are not set yet it works to insert them more than once). Hopefully this will be fixed soon so that either SwiftData ignores the second insert or even better that we get a compile time error but I am not sure if that is possible.

    So to go through the scenarios in the question:

    Insert dependent object first

    context.insert(dept) 
    context.insert(parentClass)
    

    Here the dependent is already inserted into the context when the parent init is executed and SwiftData properly acknowledges this and doesn't try to insert the dependent. This is no surprise really since it's such a common use case that one side of the relationship already exists in the context.

    Never insert the dependent object

    context.insert(parentClass) 
    

    This is already described earlier, when the parent is inserted SwiftData handles insert for the dependent. Note that this might also lead to some issues if you have a to-many relationship where you have to insert object first into a context before connecting two objects

    Insert dependent object last

    context.insert(parentClass) 
    context.insert(dept) 
    

    And here is the crash scenario, since SwiftData has already inserted the depdendent object into the context the last insert will cause a crash.


    All in all, order is important and specially for to-many relationships but what caused the crash here is that an object was inserted more than once into the model context.