Snapshotting a view that has not been rendered at least once requires afterScreenUpdates:YES

I'm just trying to save a relationship in Core Data using SwiftUI.



`TodoListView`:



struct TodoListView: View {

@Environment(\.managedObjectContext) var managedObjectContext

@FetchRequest(

entity: TodoList.entity(),

sortDescriptors: [NSSortDescriptor(key: "order", ascending: true)]

) var todoLists: FetchedResults<TodoList>

@State var add = false

@State var edit = false

var body: some View {

NavigationView {

List {

ForEach(todoLists, id: \.self) {todoList in

NavigationLink(destination: TodoItemView(todoList: todoList), label: {

Text(todoList.title!).foregroundColor(stringToColor(string: todoList.color!))

})

}

}

...



This creates navigation links for each todo list. The todo list is passed to the todo item view.



`TodoItemView`:



struct TodoItemView: View {

@Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>

@Environment(\.managedObjectContext) var managedObjectContext

@State var add = false

var todoList: TodoList

var body: some View {

List {

ForEach(todoList.todoItems!, id: \.self) {todoItem in

Text(todoItem.title!)

}

}

.navigationBarTitle(todoList.title!)

.navigationBarItems(trailing:

HStack(spacing: 15) {

Button(action: {

self.add = true

}, label: {

Image(systemName: "plus")

.imageScale(.large)

.foregroundColor(todoList.color == "None" ? .accentColor : .primary)

}).sheet(isPresented: $add, content: {

AddTodoItemView(todoList: self.todoList)

.environment(\.managedObjectContext, self.managedObjectContext)

})

...



This is supposed to list all of the todo items for the todo list. When you click on the plus image in the navigation bar, it loads a modal for adding a new todo item, and passes the todo list to it.



I should note that `todoList.todoItems` is set up as `@NSManaged public var todoItems: [TodoItem]?` in my `TodoList` entity class. I've also set it up as a one-to-many relationship in Core Data.



`AddTodoItemView`:



struct AddTodoItemView: View {

@Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>

@Environment(\.managedObjectContext) var managedObjectContext

@State var title = ""

var todoList: TodoList

var body: some View {

NavigationView {

Form {

Section {

TextField("Title", text: $title)

}

Section {

Button(action: {

self.save()

self.presentationMode.wrappedValue.dismiss()

}, label: {

Text("Save")

})

}

}

.navigationBarTitle("Add Todo Item", displayMode: .inline)

}

}

func save() {

self.title = self.title.trimmingCharacters(in: .whitespaces)

if (self.title.count > 0) {

let todoItem = TodoItem(context: managedObjectContext)

todoItem.title = self.title

todoItem.order = (self.todoList.todoItems?.last?.order ?? 0) + 1

todoList.addToTodoItems(todoItem)

do {

try self.managedObjectContext.save()

} catch {

print(error)

}

}

}

...



The `save()` function here is what causes the error and crashes the app.



I'm not sure I'm doing this correctly. Is this the right way to list and save a relationship? All I want to do is add the new todo item to the todo list. I'm having a hard time grasping working with relationships in SwiftUI.



Here is the complete result of `print(error)`:



2019-11-09 00:02:43.989211-0500 ColorTodo4[10656:682980] [Snapshotting] Snapshotting a view (0x7ff3dfd310e0, _UIReplicantView) that has not been rendered at least once requires afterScreenUpdates:YES.

2019-11-09 00:02:57.142381-0500 ColorTodo4[10656:682980] [Snapshotting] Snapshotting a view (0x7ff3dff10a20, _UIReplicantView) that has not been rendered at least once requires afterScreenUpdates:YES.

2019-11-09 00:03:08.307765-0500 ColorTodo4[10656:682980] [Snapshotting] Snapshotting a view (0x7ff3dfd307e0, _UIReplicantView) that has not been rendered at least once requires afterScreenUpdates:YES.

2019-11-09 00:03:24.827731-0500 ColorTodo4[10656:682980] [Snapshotting] Snapshotting a view (0x7ff3dfe7ca10, _UIReplicantView) that has not been rendered at least once requires afterScreenUpdates:YES.

2019-11-09 00:03:40.275685-0500 ColorTodo4[10656:682980] [Snapshotting] Snapshotting a view (0x7ff3dfd3cb80, _UIReplicantView) that has not been rendered at least once requires afterScreenUpdates:YES.

2019-11-09 00:03:41.202375-0500 ColorTodo4[10656:682980] -[__NSCFSet objectAtIndex:]: unrecognized selector sent to instance 0x60000228d3e0

2019-11-09 00:03:41.207166-0500 ColorTodo4[10656:682980] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[__NSCFSet objectAtIndex:]: unrecognized selector sent to instance 0x60000228d3e0'

*** First throw call stack:

(

0 CoreFoundation 0x00007fff23c4f02e __exceptionPreprocess + 350

1 libobjc.A.dylib 0x00007fff50b97b20 objc_exception_throw + 48

2 CoreFoundation 0x00007fff23c6ff94 -[NSObject(NSObject) doesNotRecognizeSelector:] + 132

3 CoreFoundation 0x00007fff23c53dac ___forwarding___ + 1436

4 CoreFoundation 0x00007fff23c55f38 _CF_forwarding_prep_0 + 120

5 libswiftCore.dylib 0x00007fff5105b79e $ss12_ArrayBufferV19_getElementSlowPathyyXlSiF + 142

6 libswiftCore.dylib 0x00007fff5105e6ea $sSayxSicir + 202

7 libswiftCore.dylib 0x00007fff5105ede8 $sSayxGSlsSly7ElementQz5IndexQzcirTW + 56

8 SwiftUI 0x00007fff2c4933a1 $s7SwiftUI7ForEachV11IDGeneratorO6makeID4data5index6offsetq_x_5IndexQzSitF + 289

9 SwiftUI 0x00007fff2c4963e1 $s7SwiftUI19DynamicContentState33_4103B39A1695DB4F1CFCE0B3FB46910FLLC4item2at6offsetAD4ItemCyxq_q0__G5IndexQz_SitF + 865

10 SwiftUI 0x00007fff2c497042 $s7SwiftUI19DynamicContentState33_4103B39A1695DB4F1CFCE0B3FB46910FLLC20fetchViewsPerElementSiSgyF + 466

11 SwiftUI 0x00007fff2c497dbc $s7SwiftUI19DynamicContentState33_4103B39A1695DB4F1CFCE0B3FB46910FLLC7viewIDsAA12_ViewList_IDV5ViewsCSgvg + 188

12 SwiftUI 0x00007fff2c49a732 $s7SwiftUI18DynamicContentList33_4103B39A1695DB4F1CFCE0B3FB46910FLLV5countSivgTm + 18

13 SwiftUI 0x00007fff2c49acb3 $s7SwiftUI18DynamicContentList33_4103B39A1695DB4F1CFCE0B3FB46910FLLVyxq_q0_GAA04ViewE0A2aFP5countSivgTWTm + 51

14 SwiftUI 0x00007fff2c504f59 $s7SwiftUI14MergedViewList33_70E71091E926A1B09B75AAEB38F5AA3FLLV7viewIDsAA01_dE3_IDV5ViewsCSgvg + 217

15 SwiftUI 0x00007fff2c601d16 $s7SwiftUI8SectionsV4from10useFootersAcA22_VariadicView_ChildrenV_SbtcfC + 70

16 SwiftUI 0x00007fff2c203ab9 $s7SwiftUI20SystemListDataSourceV_5style12minRowHeight0h6HeaderJ0ACyxGAA22_VariadicView_ChildrenV_So07UITableM5StyleV12CoreGraphics7CGFloatVSgANtcfC + 105

17 SwiftUI 0x00007fff2c203e8a $s7SwiftUI9PlainListV11BodyContentV4bodyQrvg + 506

18 SwiftUI 0x00007fff2c203f49 $s7SwiftUI9PlainListV11BodyContentVyx_qd__GAA4ViewA2aGP4body0E0QzvgTW + 9

19 SwiftUI 0x00007fff2c0613c7 $s7SwiftUI19DynamicPropertyBody33_9F92ACD17B554E8AB7D29ABB1E796415LLV6update7contexty14AttributeGraph0P7ContextVyADyxGGz_tF + 1671

20 SwiftUI 0x00007fff2c061bf0 $s7SwiftUI19DynamicPropertyBody33_9F92ACD17B554E8AB7D29ABB1E796415LLVyxG14AttributeGraph07UntypedN0AafGP7_update_5graph9attributeySv_So10AGGraphRefaSo11AGAttributeatFZTW + 32

21 AttributeGraph 0x00007fff2f8bbc69 $sTA + 25

22 AttributeGraph 0x00007fff2f8a3ac5 _ZN2AG5Graph11UpdateStack6updateEv + 1111

23 AttributeGraph 0x00007fff2f8a3d83 _ZN2AG5Graph16update_attributeEjb + 377

24 AttributeGraph 0x00007fff2f8a89a1 _ZN2AG8Subgraph6updateEj + 929

25 SwiftUI 0x00007fff2c19a4a0 $s7SwiftUI9ViewGraphC14runTransaction33_D63C4EB7F2B205694B6515509E76E98BLL2inySo10AGGraphRefa_tF + 224

26 SwiftUI 0x00007fff2c19a270 $s7SwiftUI9ViewGraphC17flushTransactionsyyFySo10AGGraphRefaXEfU_ + 256

27 SwiftUI 0x00007fff2c199f0f $s7SwiftUI9ViewGraphC17flushTransactionsyyF + 223

28 SwiftUI 0x00007fff2c19a08f $s7SwiftUI9ViewGraphC16asyncTransaction_8mutation5styleyAA0F0V_xAA01_D14Mutation_StyleOtAA0dI0RzlFyycfU_yACXEfU_ + 15

29 SwiftUI 0x00007fff2c198219 $s7SwiftUI9ViewGraphCIgg_ACytIeggr_TR03$s7a3UI9cD92C16asyncTransaction_8mutation5styleyAA0F0V_xAA01_D14Mutation_StyleOtAA0dI0RzlFyycfU_yACXEfU_Tf3nnpf_n + 9

30 SwiftUI 0x00007fff2c4e0817 $s7SwiftUI16ViewRendererHostPAAE06updateC5Graph4bodyqd__qd__AA0cG0CXE_tlF + 71

31 SwiftUI 0x00007fff2c4e07c3 $s7SwiftUI14_UIHostingViewCyqd__GAA0D13GraphDelegateA2aEP06updatedE04bodyqd__qd__AA0dE0CXE_tlFTW + 19

32 SwiftUI 0x00007fff2c19a06a $s7SwiftUI9ViewGraphC16asyncTransaction_8mutation5styleyAA0F0V_xAA01_D14Mutation_StyleOtAA0dI0RzlFyycfU_ + 122

33 SwiftUI 0x00007fff2c1b7cec $sIeg_ytIegr_TR + 12

34 SwiftUI 0x00007fff2bffc051 $sIeg_ytIegr_TRTA + 17

35 SwiftUI 0x00007fff2bffbf77 $sSo9NSRunLoopC7SwiftUIE14flushObserversyyFZ + 119

36 SwiftUI 0x00007fff2bffbef9 $sSo9NSRunLoopC7SwiftUIE11addObserveryyyycFZySo05CFRunbF3RefaSg_So0gB8ActivityVSvSgtcfU_ + 9

37 SwiftUI 0x00007fff2bffbfeb $sSo9NSRunLoopC7SwiftUIE11addObserveryyyycFZySo05CFRunbF3RefaSg_So0gB8ActivityVSvSgtcfU_To + 43

38 CoreFoundation 0x00007fff23bb1617 __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__ + 23

39 CoreFoundation 0x00007fff23bac0ae __CFRunLoopDoObservers + 430

40 CoreFoundation 0x00007fff23bac72a __CFRunLoopRun + 1514

41 CoreFoundation 0x00007fff23****16 CFRunLoopRunSpecific + 438

42 GraphicsServices 0x00007fff38438bb0 GSEventRunModal + 65

43 UIKitCore 0x00007fff4784fb68 UIApplicationMain + 1621

44 ColorTodo4 0x0000000107668d2b main + 75

45 libdyld.dylib 0x00007fff51a1dc25 start + 1

46 ??? 0x0000000000000001 0x0 + 1

)

libc++abi.dylib: terminating with uncaught exception of type NSException

(lldb)



Why is this happening and how do I fix it?

Accepted Reply

First of all, the snapshot message (and thus the title of your question) is unrelated to the crash. The four instances in your log happened over the course of about an hour, and the last one happened almost a full second before the crash occurred; it's just book-keeping (although someone in SwiftUI should be trying to fix whatever's causing it to complain).


As to the actual crash, I believe the issue is in your custom entity declaration. You've say you've defined the relationship property like so:


@NSManaged public var todoItems: [TodoItem]?


However, relationships in CoreData are not stored as arrays, they're sets—which don't have numeric indices. This is what's causing the crash, as noted here:


-[__NSCFSet objectAtIndex:]: unrecognized selector sent to instance 0x60000228d3e0


So, here's your save() function:


func save() {
    self.title = self.title.trimmingCharacters(in: .whitespaces)
    
    if (self.title.count > 0) {
        let todoItem = TodoItem(context: managedObjectContext)
        todoItem.title = self.title
        todoItem.order = (self.todoList.todoItems?.last?.order ?? 0) + 1
        todoList.addToTodoItems(todoItem)
       
        do {
            try self.managedObjectContext.save()
        } catch {
            print(error)
        }
    }
}


Line 7 is the one to watch here. Let's look at the various steps happening on this line:


  1. It reads the TodoItem's `todoList` property to read an object of type `TodoList`.
  2. It reads the TodoList's `todoItems` property to read an object of type `NSSet<TodoItem>`.
  3. It fetches the last item from the—nope, that's an NSSet, it doesn't have a 'last' nor a 'first' item.


At step three your app crashes.


Now, you can actually get an array from CoreData, by using a fetched property, but that's not an ideal solution for what you're seeking here. What you'll likely want to do is either use an ordered relationship (this uses NSOrderedSet under the hood, which does use first/last/index) or to manually sort the items in the set, or—even better—use one of the built-in collection functions to locate the highest `order` value.


For instance, this version of your method ought to do what you want:


func save() {
    self.title = self.title.trimmingCharacters(in: .whitespaces)
    
    if (self.title.count > 0) {
        let highestOrder = self.todoList.todoItems?.map({ $0.order }).max() ?? 0
        let todoItem = TodoItem(context: managedObjectContext)
        todoItem.title = self.title
        todoItem.order = highestOrder + 1
        todoList.addToTodoItems(todoItem)
       
        do {
            try self.managedObjectContext.save()
        } catch {
            print(error)
        }
    }
}


That's what I use in my simple non-CoreData store, so it ought to work for you.


However, I feel honor-bound to warn you: be careful with your managed object contexts here, and make judicious use of `perform()` or `performAndWait()` for things like this. It's quite possible for this function to look through a stale list of items and select an ordering that already exists, and that may cause strange behavior if you expect them to always be unique.

Replies

First of all, the snapshot message (and thus the title of your question) is unrelated to the crash. The four instances in your log happened over the course of about an hour, and the last one happened almost a full second before the crash occurred; it's just book-keeping (although someone in SwiftUI should be trying to fix whatever's causing it to complain).


As to the actual crash, I believe the issue is in your custom entity declaration. You've say you've defined the relationship property like so:


@NSManaged public var todoItems: [TodoItem]?


However, relationships in CoreData are not stored as arrays, they're sets—which don't have numeric indices. This is what's causing the crash, as noted here:


-[__NSCFSet objectAtIndex:]: unrecognized selector sent to instance 0x60000228d3e0


So, here's your save() function:


func save() {
    self.title = self.title.trimmingCharacters(in: .whitespaces)
    
    if (self.title.count > 0) {
        let todoItem = TodoItem(context: managedObjectContext)
        todoItem.title = self.title
        todoItem.order = (self.todoList.todoItems?.last?.order ?? 0) + 1
        todoList.addToTodoItems(todoItem)
       
        do {
            try self.managedObjectContext.save()
        } catch {
            print(error)
        }
    }
}


Line 7 is the one to watch here. Let's look at the various steps happening on this line:


  1. It reads the TodoItem's `todoList` property to read an object of type `TodoList`.
  2. It reads the TodoList's `todoItems` property to read an object of type `NSSet<TodoItem>`.
  3. It fetches the last item from the—nope, that's an NSSet, it doesn't have a 'last' nor a 'first' item.


At step three your app crashes.


Now, you can actually get an array from CoreData, by using a fetched property, but that's not an ideal solution for what you're seeking here. What you'll likely want to do is either use an ordered relationship (this uses NSOrderedSet under the hood, which does use first/last/index) or to manually sort the items in the set, or—even better—use one of the built-in collection functions to locate the highest `order` value.


For instance, this version of your method ought to do what you want:


func save() {
    self.title = self.title.trimmingCharacters(in: .whitespaces)
    
    if (self.title.count > 0) {
        let highestOrder = self.todoList.todoItems?.map({ $0.order }).max() ?? 0
        let todoItem = TodoItem(context: managedObjectContext)
        todoItem.title = self.title
        todoItem.order = highestOrder + 1
        todoList.addToTodoItems(todoItem)
       
        do {
            try self.managedObjectContext.save()
        } catch {
            print(error)
        }
    }
}


That's what I use in my simple non-CoreData store, so it ought to work for you.


However, I feel honor-bound to warn you: be careful with your managed object contexts here, and make judicious use of `perform()` or `performAndWait()` for things like this. It's quite possible for this function to look through a stale list of items and select an ordering that already exists, and that may cause strange behavior if you expect them to always be unique.