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

9
我有一个NavigationView,其中包含一个列表,显示来自CoreData FetchRequest的任务。FetchRequest按Task.dueDate升序排序。TaskDetail视图基本上由标题的TextField和日期选择器组成。更改详细视图中的值有效。但是,每次尝试更改日期值时,我都会得到一些奇怪的行为。日期会更改,但导航视图会自动退出详细视图并返回到列表视图。仅当我以某种方式更改日期以使列表由于排序而重新排列时才会发生这种情况。
如何防止上述奇怪的行为?

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

创建一个可运行的最小化可复现版本,需要执行以下步骤:
  1. 创建一个新的Xcode“Single View App”项目。确保勾选了CoreData复选框。
  2. 将上面的ContentView代码复制并粘贴/替换到ContentView.swift中。
  3. 创建一个名为Task的新Swift文件。复制Task的代码并粘贴到Task.swift中。
  4. 根据下图在ProjectName.xcdatamodeld中添加实体。
  5. 运行

enter image description here

我使用的是 Xcode 11.4 版本。

如果您需要更多信息,请告诉我。 非常感谢您的帮助!谢谢!


据我理解,您的操作导致从堆栈中删除了导航链接。虽然可能性较低,但请尝试设置 NavigationLink().id(task)。如果不起作用,则更改设计,例如编辑应用于结束编辑时将数据对象应用于数据库的临时数据对象。 - Asperi
能否给我们展示一份可复制、可编译的代码,这样我们就不用猜测了吗? - Chris
@Chris 谢谢回复。我添加了更多的代码。 - iSebbeYT
我并不是指在UI层面上进行制作...无论如何,它需要一些可重现的示例来进行测试。 - Asperi
@Asperi 好的。我更新了代码,并提供了一些关于如何设置可重现示例的说明。 - iSebbeYT
显示剩余6条评论
2个回答

5

更新2(iOS 14 beta 3)

iOS 14 beta 3中似乎已经修复了该问题:当进行影响排序顺序的更改时,详细视图不再弹出。


更新

苹果似乎将此视为功能而非漏洞; 今天他们回复了我的反馈(FB7651251)如下:

如果您希望获得此行为,请使用isActive并使用选择绑定来自行管理推送。按照当前模式,该应用程序的行为是正确的。

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


如我在上面的评论中提到的,我认为这是中的漏洞。

解决方法可以是在列表之外使用NavigationLink,并将列表行定义为按钮。

a) 设置要编辑的任务(新@State变量selectedTask)

b) 触发导航链接到TaskDetail(task:selectedTask!)

这个设置将使所选任务与其在已排序列表中的位置分离,从而避免由于重新排序而可能由于编辑dueDate而引起的错误行为。

要实现此目标:

  1. 将这两个@State变量添加到struct ContentView中
    @State private var selectedTask: Task?
    @State private var linkIsActive = false

更新 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()})
        }
    }

将以下结构添加到ContentView.swift中:
    struct linkDestination: View {
        let selectedTask: Task?
        var body: some View {
            return Group {
                if selectedTask != nil {
                    TaskDetail(task: selectedTask!)
                } else {
                    EmptyView()
                }
            }
        }
    }

我喜欢被客户大声呵斥,因为苹果决定了一些破坏性的功能...这样的更改应该如何处理生产中的应用程序? - Majid

1

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

当链接处于活动状态时,我使Fetchrequest动态化并更改了sortdescriptor。这个做法的负面影响是每次导航回到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()})
}

如果我理解正确,列表在你离开详细视图之前不会被排序?但是当你在iPad上运行应用程序并且主视图和详细视图同时显示时会发生什么?它不会变得奇怪吗? - iSebbeYT
是的,你说得对。这在主从视图中不会正常工作。我没有考虑到这一点。 - Fabian M

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接