1
votes

I am using a TabView/NavigationView & NavigationLink to programmatically navigate from a list view to detailed view and when I update the boolean property 'pinned' to true & save the core data entity in the detailed view, I am getting the following unfortunate side effects:

  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 have prepared a small Xcode project with the complete sample code

In the list view I use @FetchRequest to query the list and sort on the following:

    @FetchRequest(entity: Task.entity(),sortDescriptors: [NSSortDescriptor(key: "pinned", ascending: false),
                                                          NSSortDescriptor(key: "created", ascending: true),
                                                          NSSortDescriptor(key: "name", ascending: true)])

In the list view I use the following:

List() {
   ForEach(tasks, id: \.self) { task in
       NavigationLink(destination: DetailsView(task: task), tag: task.id!.uuidString, selection: self.$selectionId) {
         HStack() {
            ...
         }
      }
}

.

(1) If I omit the 'NSSortDescriptor(key: "pinned" ...)' I don't see the behavior.

(2) If I omit the 'tag:' and the 'selection:' parameters in the NavigationLink() I don't see the behavior. But I need to be able to trigger the navigation link programmatically when I create a new Task entity.

(3) It seems never to happen when I have a single entity in the list or changing the value of the 'pinned' boolean property in the first entity in the list.

(4) I get the 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)...

The parent view to the list view (TasksListView) contains a TabView:

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
                    }
                }
                
        }
    }
}

struct DetailsView: View {
    
    // NSManagedObjectContext
    @Environment(\.managedObjectContext) var viewContext
        
    @ObservedObject var task : Task
    
    @State var name : String = ""
    @State var dueDate : Date = Date()
    @State var hasDueDate : Bool = false
    @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))
                Text("\(task.id?.uuidString ?? "unknown")")
                    .font(Font.headline.weight(.light))
            }
            
            Section() {
                HStack() {
                    Text("Created")
                    Spacer()
                    Text("\(task.created ?? Date(), formatter: Self.dateFormatter)")
                        .font(Font.subheadline.weight(.light))
                }
            
                Toggle(isOn: self.$hasDueDate) {
                    Text("Set Due Date")
                }
                
                if self.hasDueDate {
                    DatePicker("Due Date", selection: self.$dueDate, in: Date()... , displayedComponents: [.hourAndMinute, .date])
                }
            }
        }
        .navigationBarTitle(Text("Task Details"),displayMode: .inline)
        .navigationBarItems(trailing: rightButton)
        .listStyle(GroupedListStyle())
        .onAppear() {
            if self.task.pinned {
                self.isPinned = true
            }
            if self.task.name != nil {
                self.name = self.task.name!
            }
            if self.task.due != nil {
                self.dueDate = self.task.due!
                self.hasDueDate = true
            }
        }
        
    }
    
    // save button
    
    var rightButton: some View {
        
        Button("Save") {

            // save values in task & save:
            self.task.pinned = self.isPinned
            if self.hasDueDate  {
                self.task.due = self.dueDate
            }
            
            if self.name.count > 0 {
                self.task.name = self.name
            }
            
            Task.save(in: self.viewContext) { (success, error) in
                DispatchQueue.main.async {
                    if success {
                        print("Task saved")
                    }
                    else {
                        print("****** Error: Task can't be saved, error = \(error!.localizedDescription)")
                    }
                }
            }
        }
        .contentShape(Rectangle())        
    }
    
    
}

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
                print("****** Error: Unresolved error \(nserror), \(nserror.userInfo)")
                completion(false, nserror)
            }
        }
     }
}




Any suggestions?

2

2 Answers

0
votes

You seem to be creating your tab view in a different way than me. Those extra navigation views made cause an issue.

Not sure if it will help or not but I do it like that:

struct ContentView: View {
var body: some View {
    TabView {
         TasksListView()
            .tabItem {
                Image(systemName: "tray.full")
                    .font(.title)
                Text("Master")
        }

            EmptyView()
              .tabItem {
                  Image(systemName: "magnifyingglass")
                      .font(.title)
                  Text("Search")
        }
    }  
}

}

0
votes

This is a bug in iOS 13.

It has been fixed since iOS 14.0 beta 3.

You will find a similar question here (the accepted answer provides a workaround):

Issue when rearranging List item in detail view using SwiftUI Navigation View and Sorted FetchRequest