Spurious pop and re-push during state changes

Hi,


I've got a hard-to-reproduce bug where my views pop off the `NavigationView` stack and then re-push during state changes. I'm not sure what the heck is causing it yet. If this is a familiar problem to anyone, please let me know.


In other words I have a navigation stack like: "Root > A > B". I edit something in B and I get an unwanted pop animation back to "Root > A", and then it immediately pushs again so it's back to "Root > A > B".


~ R

You should show the code where you fire the navigation links.


May be you miss some id declaration like in this example (most likely your situation is different, just to illustrate):

    var body: some View {
        NavigationView {
            List {
                ForEach (0 ..< itemsCount, id: \.self) { index in

Ah, thanks. By "id declaration”, do you mean making my objects implement `Identifiable`?


I do have some code that looks like your example, I the code below, the `FetchResultsHelp` is doing a Core Data query (NSFetchRequest), and the `exercises` are NSManagedObjects. A StackOverflow answer recommend using the `id: \.self` in the ForEach, but maybe that’s wrong.


I just tried making the NSManagedObject (Exercise) implement `Identifiable` by using the `objectID: NSManagedObjectID`. That _changes_ the behavior, but it’s still screwy. Instead of the “B” popping and re-pushing. I now get an animation that looks like a second “B” pushing, followed by a pop back to “Root > A”. (The “A” view is the list of objects.)


var body: some View {
    ...   
    FetchedResultsHelp(type: Exercise.self,
                       predicate: makePredicate(),
                       sortDescriptors: makeSortDescriptors()) { exercises in
       ForEach(exercises, id: \.self) { ex in
           NavigationLink(destination: self.exerciseView(for: ex)) {
               Text(ex.name ?? “-“)
           }
       }
       ...
}


I changed it to:


ForEach(exercises) { ex in

By "id declaration”, do you mean making my objects implement `Identifiable`?


I meant adding id: \self

ForEach(exercises, id: \.self) { ex in


I understand you suppressed it but it was there originally.


Could you show the complete code ?

There's a lot of code. I'm trying to boil it down now, so I can post it.


Of note: I'm getting the following warning from SwiftUI on the console. I set the breakpoint it advises, and I see that breakpoint is getting hit right before the my problem occurs.


2020-06-14 11:24:37.906650-0500 MyAppName[11953:3649945] [TableView] Warning once only: UITableView was told to layout its visible cells and other contents without being in the view hierarchy (the table view or one of its superviews has not been added to a window). This may cause bugs by forcing views inside the table view to load and perform layout without accurate information (e.g. table view bounds, trait collection, layout margins, safe area insets, etc), and will also cause unnecessary performance overhead due to extra layout passes. Make a symbolic breakpoint at UITableViewAlertForLayoutOutsideViewHierarchy to catch this in the debugger and see what caused this to occur, so you can avoid this action altogether if possible, or defer it until the table view has been added to a window. Table view: <_TtC7SwiftUIP33_BFB370BA5F1BADDC9D83021565761A4925UpdateCoalescingTableView: 0x102080200; baseClass = UITableView; frame = (0 0; 320 567.5); clipsToBounds = YES; autoresize = W+H; gestureRecognizers = <NSArray: 0x280b3bb70>; layer = <CALayer: 0x2805fbae0>; contentOffset: {0, 144.5}; contentSize: {320, 704}; adjustedContentInset: {64, 0, 0, 0}; dataSource: <_TtGC7SwiftUIP10$1b8848e2419ListCoreCoordinatorGVS_20SystemListDataSourceOs5Never_GOS_19SelectionManagerBoxS2___: 0x101265b80>>


When the breakpoint is hit, the entire stack (pasted below) is Apple's code, not mine. So it's hard to see if I'm causing this, or if it's a bug in SwiftUI.


#00x0000000185947f5c in UITableViewAlertForLayoutOutsideViewHierarchy ()
#10x0000000185946fb0 in -[UITableView _updateVisibleCellsNow:] ()
#20x0000000185959b0c in -[UITableView _cellForRowAtIndexPath:usingPresentationValues:] ()
#30x00000001859599ec in -[UITableView cellForRowAtIndexPath:] ()
#40x00000001b851525c in ListCoreCoordinator.updateListContents(_:) ()
#50x00000001b8514560 in ListCoreCoordinator.updateUITableView(_:to:) ()
#60x00000001b8512ff0 in ListRepresentable.updateUIView(_:context:) ()
#70x00000001b87d98f4 in PlatformViewRepresentableAdaptor.updateViewProvider(_:context:) ()
#80x00000001b85894f0 in closure #1 in closure #1 in PlatformViewChild.update(context:) ()
#90x00000001b86e8d28 in ViewRendererHost.performExternalUpdate(_:) ()
#100x00000001b8588a18 in perform #1 <A>(work:) in closure #1 in PlatformViewChild.update(context:) ()
#110x00000001b85878c8 in closure #1 in PlatformViewChild.update(context:) ()
#120x00000001b85816c4 in PlatformViewChild.update(context:) ()
#130x00000001b8589a40 in protocol witness for static UntypedAttribute._update(_:graph:attribute:) in conformance PlatformViewChild<A> ()
#140x00000001acd0c998 in partial apply ()
#150x00000001accf59fc in AG::Graph::UpdateStack::update() ()
#160x00000001accf5eb4 in AG::Graph::update_attribute(unsigned int, bool) ()
#170x00000001accfac30 in AG::Subgraph::update(unsigned int) ()
#180x00000001b84531dc in ViewGraph.runTransaction(in:) ()
#190x00000001b8452fa0 in closure #1 in ViewGraph.flushTransactions() ()
#200x00000001b8452c44 in ViewGraph.flushTransactions() ()
#210x00000001b8452d9c in closure #1 in closure #1 in ViewGraph.asyncTransaction<A>(_:mutation:style:) ()
#220x00000001b86e89f8 in ViewRendererHost.updateViewGraph<A>(body:) ()
#230x00000001b8452d68 in closure #1 in ViewGraph.asyncTransaction<A>(_:mutation:style:) ()
#240x00000001b846d804 in thunk for @escaping @callee_guaranteed () -> () ()
#250x00000001b82f68b4 in static NSRunLoop.flushObservers() ()
#260x00000001b82f682c in closure #1 in static NSRunLoop.addObserver(_:) ()
#270x00000001b82f6948 in @objc closure #1 in static NSRunLoop.addObserver(_:) ()
#280x000000018165a06c in __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__ ()
#290x0000000181654f60 in __CFRunLoopDoObservers ()
#300x00000001816553dc in __CFRunLoopRun ()
#310x0000000181654ce8 in CFRunLoopRunSpecific ()
#320x000000018b79f38c in GSEventRunModal ()
#330x0000000185783444 in UIApplicationMain ()

Let me add: the warning mentions UKit's `UITableView`, but I'm not using it directly. Just the SwiftUI `List`.

hi,


i've seen this type of behaviour come and go during some development, but not for some time now. and everytime i think i fixed it ... whether i knew what i was doing or not ... it involved tweaking the Identifiability of an object and the ForEach thing.


i would suggest that if you want to conform to Identifiable, that you not rely on the objectID from CoreData (i think this can change over time, and it certainly changes from run to run). instead, put a real id field in your CoreData entities, and set id = UUID() upon creation. see if that helps.


also, i'd wait a little for Swift 2.0 to come out (in about 7-8 days) to see what we find out about navigation changes. if you're using @FetchRequest, i think there will be changes there (at least internally). and, maybe some documentation ⚠


EDIT: (added after posting) don't worry too much about "table laying out ..." messages. SwiftUI is certainly built on top of UIKit. most everyone ignores messages like this these days. wait for Swift 2.0)


hope that helps,

DMG

You wrote: "[objectID] certainly changes from run to run"


I'm not seeing that. It seems to be completely consistent. I was using `objectID.uriRepresentation()` to implement `Identifiable`, and the docs for `uriRepresentation` say it is "archivable", which suggests to me that it doesn't change. (?) I'm using `NSPersistentCloudKitContainer`, if that matters.


I think that what's getting me now is...


ForEach(dataSource.fetchExercises(moc:moc)) { ex in
    NavigationLink(destination: ExerciseView(exercise: ex)) {
        ListItemView(exercise: ex)
    }
}


If that detail view (ExerciseView), changes the exercise in way that should _reorder_ the results of fetchExercises and the ForEach, then I make `dataSource` (which is an observable object) emit `objectWillChange`. As expected, SwiftUI re-calls `fetchExercises`. But sadly, it pops off the detail view (ExerciseView) and returns to this list view.


I thought it should see that the exercise objects have the same IDs, so it would keep the ExerciseView "pushed".


I too am interested to see the SwiftUI 2020 changes next week, but I can't wait until September to release my app, so I need to get this working with the current SwiftUI.

Spurious pop and re-push during state changes
 
 
Q