Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

I have a NavigationView with a list showing tasks from a CoreData FetchRequest. The FetchRequest is sorted ascending on Task.dueDate. The TaskDetail view basically consists of a TextField for the title and a date picker for the date. Changing the values in the detail view works. Though I get some weird behaviour every time I try to change the date value. The date gets changed but the Navigation view automatically exits the detail view and goes back to the list view. It only happens when I change the date in such a way that the list gets rearranged due to the sorting.

How do I prevent this weird behaviour described above??

enter image description here

//
//  ContentView.swift

import SwiftUI
import CoreData

struct ContentView: View {

    @Environment(\.managedObjectContext) var moc
    @FetchRequest(fetchRequest: Task.requestAllTasks()) var tasks: FetchedResults<Task>

    var body: some View {
        NavigationView {
            List(tasks, id: \.id) { task in
                NavigationLink(destination: TaskDetail(task: task)) {
                    Text("\(task.title)")
                }
            }.navigationBarTitle("Tasks").navigationBarItems(trailing: Button("new") {self.addTask()})
        }
    }

    func addTask() -> Void {
        let newTask = Task(context: self.moc)
        newTask.id = UUID()
        newTask.title = "task \(tasks.count)"
        newTask.dueDate = Date()
        print("created new Task")
        if (self.moc.hasChanges) {
            try? self.moc.save()
            print("saved MOC")
        }
        print(self.tasks)
    }

}

struct TaskDetail : View {

    @ObservedObject var task: Task

    var body: some View {
        VStack{
            TextField("name", text: $task.title)
            DatePicker("dueDate", selection: $task.dueDate, displayedComponents: .date)
                .labelsHidden()
        }
    }
}

//
//  Task.swift

import Foundation
import CoreData

public class Task: NSManagedObject, Identifiable {
    @NSManaged public var id: UUID?
    @NSManaged public var dueDate: Date
    @NSManaged public var title: String

    static func requestAllTasks() -> NSFetchRequest<Task> {
        let request: NSFetchRequest<Task> = Task.fetchRequest() as! NSFetchRequest<Task>

        let sortDescriptor = NSSortDescriptor(key: "dueDate", ascending: true)
        request.sortDescriptors = [sortDescriptor]

        return request
    }
}

To create a running minimal reproducible version of this...do:

  1. Create new Xcode "Single View App" Project. Make sure to check the CoreData checkbox.
  2. Copy the code for ContentView above and paste/replace in ContentView.swift.
  3. Create a new Swift file named Task. Copy the code for Task and paste in Task.swift.
  4. Add the entities in the ProjectName.xcdatamodeld according to the image below.
  5. Run

enter image description here

I am on Xcode 11.4.

Let me know if you need me to provide more information. Any help is much appreciated! Thanks!

like image 796
iSebbeYT Avatar asked Apr 03 '20 05:04

iSebbeYT


1 Answers

UPDATE 2 (iOS 14 beta 3)

The issue seems to be fixed in iOS 14 beta 3: the Detail view does no longer pop when making changes that affect the sort order.


UPDATE

It seems Apple sees this as a feature, not a bug; today they replied to my feedback (FB7651251) about this issue as follows:

We would recommend using isActive and managing the push yourself using the selection binding if this is the behavior you desire. As is this is behaving correctly.

This is because the identity of the pushed view changes when you change the sort order.


As mentioned in my comment above I believe this is a bug in iOS 13.4.

A workaround could be to use a NavigationLink outside of the List and define the List rows as Buttons that

a) set the task to be edited (a new @State var selectedTask) and

b) trigger the NavigationLink to TaskDetail(task: selectedTask!).

This setup will uncouple the selected task from its position in the sorted list thus avoiding the misbehaviour caused by the re-sort potentially caused by editing the dueDate.

To achieve this:

  1. add these two @State variables to struct ContentView
    @State private var selectedTask: Task?
    @State private var linkIsActive = false
  1. update the body of struct ContentView as follows
    var body: some View {
        NavigationView {
            ZStack {
                NavigationLink(
                    destination: linkDestination(selectedTask: selectedTask),
                    isActive: self.$linkIsActive) {
                    EmptyView()
                }
                List(tasks) { task in
                    Button(action: {
                        self.selectedTask = task
                        self.linkIsActive = true
                    }) {
                        NavigationLink(destination: EmptyView()){
                            Text("\(task.title)")
                        }
                    }
                }
            }
            .navigationBarTitle("Tasks").navigationBarItems(trailing: Button("new") {self.addTask()})
        }
    }
  1. add the following struct to ContentView.swift
    struct linkDestination: View {
        let selectedTask: Task?
        var body: some View {
            return Group {
                if selectedTask != nil {
                    TaskDetail(task: selectedTask!)
                } else {
                    EmptyView()
                }
            }
        }
    }
like image 89
mmklug Avatar answered Sep 29 '22 03:09

mmklug