【问题标题】:SwiftUI / Core Data - Involuntary navigation between list view and detailed view when updating detailed viewSwiftUI / Core Data - 更新详细视图时列表视图和详细视图之间的非自愿导航
【发布时间】:2020-11-20 11:38:30
【问题描述】:

我正在使用 TabView/NavigationView 和 NavigationLink 以编程方式从列表视图导航到详细视图,当我将布尔属性“固定”更新为 true 并将核心数据实体保存在详细视图中时,我得到以下信息不幸的副作用:

  1. 不自觉地导航回列表视图,然后再次返回详细视图
  2. 不自觉地导航到同一详细视图的另一个副本,然后返回到列表视图。

我准备了一个小Xcode project with the complete sample code

在列表视图中,我使用@FetchRequest 查询列表并按以下内容排序:

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

在列表视图中,我使用以下内容:

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

.

(1) 如果我省略 'NSSortDescriptor(key: "pinned" ...)' 我看不到这种行为。

(2) 如果我在 NavigationLink() 中省略了 'tag:' 和 'selection:' 参数,我看不到这种行为。但是当我创建一个新的任务实体时,我需要能够以编程方式触发导航链接。

(3) 当我在列表中有单个实体或更改列表中第一个实体中“固定”布尔属性的值时,似乎永远不会发生。

(4) 我收到警告:

[TableView] 仅警告一次:UITableView 被告知在视图层次结构中布局其可见单元格和其他内容(表视图或其父视图之一尚未添加到窗口中)...

列表视图(TasksListView)的父视图包含一个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)
            }
        }
     }
}




有什么建议吗?

【问题讨论】:

    标签: core-data swiftui-list swiftui-navigationlink swiftui


    【解决方案1】:

    这是iOS 13 中的一个错误。

    iOS 14.0 beta 3以来已修复。

    您会在这里找到一个类似的问题(接受的答案提供了一种解决方法):

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

    【讨论】:

    • Xcode 版本 12.0 beta 4 上述行为已修复,但仍有一些问题:当我更新布尔值/保存并使用后退按钮导航回列表视图时,应用程序无意中导航回详细视图。注意:我正在使用 Xcode 生成的带有 Core Data 项目示例的 Master-Detail 测试此行为,并将布尔属性添加到 Event 实体(以及 id:UUID),在这种情况下,我没有使用(...,标签:选择:) NavigationLink 的参数。如果我添加这些参数并以编程方式触发链接,我会得到相同的行为。
    • @abildgaard 我没有在 iOS 14.0 上测试您的问题中链接的 GitHub 项目,因为它崩溃了。 Xcode 12 beta 4 似乎不再提供 Master Detail 模板。因此,我使用 Master Detail 模板使用最新的 XCode11 创建了一个新项目,并进行了您在评论中描述的更改,并且可以重现您的问题,因此使用 DoubleColumnNavigationViewStyle() 时似乎仍未解决。您可能想向 Apple 提供反馈。我能够通过将 .id(UUID()) 添加到 MasterView 中的列表来解决/解决不当行为。这对你有用吗?
    • @Lobo 我确实向 Apple 报告过。您能否在解决方法中提供更多详细信息 - 您是指 ForEach 中的密钥路径吗?我正在使用“List { ForEach(events, id: \.id) { event in ...}}”,因为我的 Event.id 是 UUID 类型。
    • @abildgaard 我的意思是在列表中添加一个 .id 修饰符,即 List { ForEach(events, id: \.id) { event in ...}}.id(UUID()) 这个每次调用 body 属性时都会强制创建一个全新的列表,从而消除不当行为。
    【解决方案2】:

    您创建标签视图的方式似乎与我不同。这些额外的导航视图会导致问题。

    不确定它是否会有所帮助,但我会这样做:

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

    }

    【讨论】:

    • 很遗憾没有。我可以将 NavigationView() 添加到 TasksListView() 和其他视图中,但问题仍然存在。谢谢你的建议!
    • 如果我使用 Xcode 的 Master-Detail 项目模板并添加一些内容,我会看到完全相同的行为:(1) 将 BOOL 属性“固定”到核心数据实体 (2) 添加切换视图向DetailView (3) 添加保存按钮到DetailedView (4) 将NSSortDescriptor 更改为使用固定属性 当我将固定值设置并保存为true 时,它​​给了我同样不幸的副作用
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-12-18
    相关资源
    最近更新 更多