使用 SwiftUI 导航视图和排序的 FetchRequest 在详细视图中重新排列列表项时出现问题

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

我有一个 NavigationView,其中有一个列表显示来自 CoreData FetchRequest 的任务。 FetchRequest 在 Task.dueDate 上按升序排序。 TaskDetail 视图基本上由标题的 TextField 和日期的日期选择器组成。更改详细视图中的值有效。尽管每次我尝试更改日期值时都会出现一些奇怪的行为。日期已更改,但导航视图会自动退出详细视图并返回到列表视图。只有当我更改日期时列表因排序而重新排列时才会发生。

如何防止上述这种奇怪的行为?

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

要创建此的 运行 最小可复制版本...请执行以下操作:

  1. 新建 Xcode "Single View App" 项目。确保检查 CoreData 复选框。
  2. 复制上面的 ContentView 代码和 paste/replace 中的 ContentView.swift。
  3. 创建一个新的 Swift 文件,命名为 Task.复制任务代码并粘贴到 Task.swift.
  4. 根据下图添加ProjectName.xcdatamodeld中的实体。
  5. 运行

我在 Xcode 11.4.

如果您需要我提供更多信息,请告诉我。 任何帮助深表感谢!谢谢!

更新 2(iOS 14 测试版 3)

此问题似乎已在 iOS 14 beta 3 中得到修复:在进行影响排序顺序的更改时,详细信息视图不再弹出。


更新

Apple 似乎将此视为一项功能,而不是错误;今天他们回复了我关于这个问题的反馈(FB7651251)如下:

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.


正如我在上面的评论中提到的,我认为这是 iOS 13.4 中的一个错误。

一种解决方法是在列表外部使用 NavigationLink 并将列表行定义为

的按钮

a) 设置要编辑的任务(一个新的@State var selectedTask)和

b) 触发 NavigationLink 到 TaskDetail(任务:selectedTask!)。

此设置会将所选任务与其在排序列表中的位置分离,从而避免因编辑 dueDate 可能导致的重新排序而导致的不当行为。

实现这个:

  1. 将这两个@State 变量添加到 struct ContentView
    @State private var selectedTask: Task?
    @State private var linkIsActive = false
  1. 更新结构体 ContentView 如下
    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. 将以下结构添加到 ContentView.swift
    struct linkDestination: View {
        let selectedTask: Task?
        var body: some View {
            return Group {
                if selectedTask != nil {
                    TaskDetail(task: selectedTask!)
                } else {
                    EmptyView()
                }
            }
        }
    }

我遇到了同样的问题,找不到好的解决办法。 但我有另一种解决方法。

当 link 处于活动状态时,我使 Fetchrequest 动态化并更改了排序描述符。 这会产生负面影响,即每次您导航回 ContentView 时,列表都会使用动画对自身进行排序。

如果您为列表添加以下结构:

struct TaskList: View {
    @Environment(\.managedObjectContext) var moc
    @FetchRequest var tasks: FetchedResults<Task>
    @Binding var activeTaskLink: Int?

    init(activeTaskLink: Binding<Int?>, currentSortKey: String) {
        self._activeTaskLink = activeTaskLink
        self._tasks = Task.requestAllTask(withSortKey: currentSortKey)
    }

    var body: some View {
        List(tasks, id: \.id) { task in
            NavigationLink(destination: TaskDetail(task: task), tag: task.objectId.hashValue, selection: self.$activeTaskLink) {
                Text("\(task.title)")
            }
        }
    }

}

然后更改Task.swift中的requestAllTask​​函数:

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

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

    return request
}

然后在ContentView中为activeTask添加状态

@State var activeTaskLink: Int? = nil

并将正文更改为

var body: some View {
    TaskList(activeTaskLink: self.$activeTaskLink, currentSortKey: self.activeNavLink != nil ? "id" : "dueDate")
    .navigationBarTitle("Tasks")
    .navigationBarItems(trailing: Button("new") {self.addTask()})
}