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

Seb*_*bbe 7 core-data list master-detail swift swiftui

我有一个带有列表的 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
    }
}
Run Code Online (Sandbox Code Playgroud)

要创建一个正在运行的最小可重现版本...请执行以下操作:

  1. 创建新的 Xcode“Single View App”项目。确保选中 CoreData 复选框。
  2. 复制上面的 ContentView 代码并在 ContentView.swift 中粘贴/替换。
  3. 创建一个名为 Task 的新 Swift 文件。复制 Task 的代码并粘贴到 Task.swift 中。
  4. 根据下图在 ProjectName.xcdatamodeld 中添加实体。

在此处输入图片说明

我在 Xcode 11.4 上。

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

Lob*_*obo 5

更新2(iOS 14 beta 3)

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


更新

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

如果这是您想要的行为,我们建议您使用 isActive 并使用选择绑定自行管理推送。这是正确的行为。

这是因为当您更改排序顺序时,推送视图的标识也会发生变化。


正如我在上面的评论中提到的,我相信这是iOS 13.4.

解决方法可能是在列表外部使用 NavigationLink 并将列表行定义为按钮

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

b) 触发 TaskDetail 的 NavigationLink(task: selectedTask!)。

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

为了达成这个:

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