SwiftUI: Involuntary ping pong when editing and saving detailed view / NavigationLink

SwiftUI / Core Data / TabView / Master Detailed View / NavigationLink


see sample project

The code is using NavigationLink(destination,tag,selection) to programmatically navigate from a master view to detailed view. Removing the tag/selection removes the problem but doesn't remove my need to trigger the navigation link programmatically.

Master view uses a @FetchRequest which includes multiple NSSortDescriptors which values are changed/saved in detailed view.

When the 'pinned' value is changed and saved (pinned is also a NSSortDescriptor for the master view's list) the app navigates involuntary from the detailed view to another instance of the detailed view before navigating back to the master view or other times navigates back to the master view and then back to the detailed view.

The only way to prevent this behavior is to remove the 'pinned' NSSortDescriptors from the @FetchRequest

I also get the following warning: [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)....

Replicate:

In the included sample app (link above) add 6 new 'tasks' and they are saved into Core Data.

Then navigate to any of the detailed task view and change the pinned value and select save. This should provoke the involuntary navigation to a new instance of the detailed view before ending up involuntary on the master view or other times navigates back to the master view and then back to the detailed view.


MacOS 10.15.6 / Xcode Version 11.6 / iOS 13.6

Replies

I am getting the following behavior:

  1. an involuntary navigation back to the list view and then again back to the detailed view or

  2. an involuntary navigation to another copy of the same detailed view and then back to the list view.

I am adding the code below :

Code Block swiftui
struct ContentView: View {
var body: some View {
TabView {
NavigationView {
TasksListView()
}
.tabItem {
Image(systemName: "tray.full")
.font(.title)
Text("Master")
}
NavigationView {
EmptyView()
}
.tabItem {
Image(systemName: "magnifyingglass")
.font(.title)
Text("Search")
}
}
}
}
struct TasksListView: View {
// NSManagedObjectContext
@Environment(\.managedObjectContext) var viewContext
// Results of fetch request for tasks:
@FetchRequest(entity: Task.entity(),sortDescriptors: [NSSortDescriptor(key: "pinned", ascending: false),
NSSortDescriptor(key: "created", ascending: true),
NSSortDescriptor(key: "name", ascending: true)])
var tasks: FetchedResults<Task>
// when we create a new task and navigate to it programitically
@State var selectionId : String?
@State var newTask : Task?
var body: some View {
List() {
ForEach(tasks, id: \.self) { task in
NavigationLink(destination: DetailsView(task: task), tag: task.id!.uuidString, selection: self.$selectionId) {
HStack() {
VStack(alignment: .leading) {
Text("\(task.name ?? "unknown")")
.font(Font.headline.weight(.light))
.padding(.bottom,5)
Text("Created:\t\(task.created ?? Date(), formatter: Self.dateFormatter)")
.font(Font.subheadline.weight(.light))
.padding(.bottom,5)
if task.due != nil {
Text("Due:\t\t\(task.due!, formatter: Self.dateFormatter)")
.font(Font.subheadline.weight(.light))
.padding(.bottom,5)
}
}
}
}
}
}
.navigationBarTitle(Text("Tasks"),displayMode: .inline)
.navigationBarItems(trailing: rightButton)
}
var rightButton: some View {
Image(systemName: "plus.circle")
.foregroundColor(Color(UIColor.systemBlue))
.font(.title)
.contentShape(Rectangle())
.onTapGesture {
// create a new task and navigate to it's detailed view to add values
Task.create(in: self.viewContext) { (task, success, error) in
if success {
self.newTask = task
self.selectionId = task!.id!.uuidString
}
}
}
}
}
-- continued

The details view:

Code Block swiftui
struct DetailsView: View {
    @Environment(\.managedObjectContext) var viewContext
    @ObservedObject var task : Task
    @State var isPinned : Bool = false
    var body: some View {
        List() {
            Section() {
                Toggle(isOn: self.$isPinned) {
                    Text("Pinned")
                }
            }
            Section() {
                TextField("Name", text: self.$name)
                    .font(Font.headline.weight(.light)
            }
....
        }
        .navigationBarTitle(Text("Task Details"),displayMode: .inline)
        .navigationBarItems(trailing: rightButton)
        .listStyle(GroupedListStyle())
        .onAppear() {
            if self.task.pinned {
                self.isPinned = true
            }
        }
    }
    var rightButton: some View {
        Button("Save") {
            self.task.pinned = self.isPinned
Task.save(in: self.viewContext) { (success, error) in
                DispatchQueue.main.async {
....
                }
            }
        }
    }
}

Code Block swift
extension Task {
    static func save(in managedObjectContext: NSManagedObjectContext, completion: @escaping (Bool, NSError?) -> Void ) {
        managedObjectContext.performAndWait() {
            do {
                try managedObjectContext.save()
                completion(true, nil)
            } catch {
                let nserror = error as NSError
                completion(false, nserror)
            }
        }
     }
}


I am seeing precisely the same behavior if I use Xcode's Master-Detail project template and make a few additions:
  • added BOOL property 'pinned' to the core data Entity

  • added Toggle view to the DetailView

  • added save button to the DetailedView

  • changed the NSSortDescriptor to use pinned property

It gives me the same unfortunate side effects when I set & save the pinned value to true
  • an involuntary navigation back to the list view and then again back to the detailed view or

  • an involuntary navigation to another copy of the same detailed view and then back to the list view.

Fixed in Xcode 12/Beta 4 (maybe even in 3)