首页 > 解决方案 > 如何在获取核心数据之后但在显示表视图之前更新依赖于当前时间的字段?

问题描述

问题总结

我正在编写一个 iOS 应用程序,它就像一个待办事项列表和日历的混合体。与待办事项列表不同,您不会将事情检查为完整。而是将待办事项插入 UITableView 中的活动和非活动部分。问题是部分和排序可能随时相对于当前日期和时间发生变化。

因此,在获取数据之后但在视图控制器开始显示它之前,我目前最苦恼的是如何以及何时在初始应用程序加载时刷新 section/nextAlert 日期。

我尝试过的背景

我已经研究并尝试了许多不同的方法来解决这些问题。

  1. 在 Fetch 之后刷新 viewDidLoad。我试图在 viewDidLoad 期间在表视图控制器中获取后立即调用 refreshReminders 函数。只要我实际上不保存到该函数中的核心数据,这似乎就可以正常工作。只需在记录/记录上设置字段即可触发控制器,并且行会按应有的方式移动和更新。虽然看起来这在加载序列中可能为时已晚。我们真的希望表格视图必须在应用程序第一次启动时移动记录吗?这听起来是个不好的做法。

现在,如果我尝试在刷新后保存对核心数据的更改,这就是它变得非常奇怪的地方。该表显然会根据当前上下文中记录的更改进行更新,但是当保存上下文时,它会再次触发更改!由于已经进行了更改,因此在尝试将行从旧位置移动到新位置后,应用程序会崩溃。请参阅下面的代码。

  1. 在获取唤醒之前刷新。我还尝试使用核心数据对象(提醒)的扩展来设置部分和 nextAlert 日期。但是,这似乎没有任何作用。即表格仍然加载错误部分和/或排序顺序中的提醒。

刷新此处的字段也不会将数据标记为“脏”(已更改),因此表视图控制器不会移动它,并且只有在数据脏时才会触发的上下文保存当然不会被触发。我什至尝试在获取后保存上下文而不检查它是否已更改,但这也无济于事。即核心数据真的不认为有任何改变,即使它已经改变了。

这是代码

这是我的 tableViewController 的控制器功能中的 .move 案例

case .move:
            if let oldPath = indexPath, let newPath = newIndexPath {
                os_log("RemindersViewController: Move was triggered, now updating row in table. Old path was %{public}@ and new path is %{public}@", log: OSLog.default, type: .info, oldPath as CVarArg, newPath as CVarArg)
RemindersCell, withReminder: anObject as! Reminders)
                configureCell(tableView.cellForRow(at: oldPath) as! RemindersCell, withReminder: anObject as! Reminders)
                os_log("RemindersViewController: updated moved cell.", log: OSLog.default, type: .info)


                // Don't actually try to move it if the old and new path are the same
                if (newPath != oldPath) {
                    os_log("RemindersViewController: Moving row in table.", log: OSLog.default, type: .info)
                    tableView.moveRow(at: oldPath, to: newPath)
                    os_log("RemindersViewController: row moved.", log: OSLog.default, type: .info)
                }
            }

这是我的 configureCell 函数的简化版本。

    func configureCell(_ cell: RemindersCell, withReminder reminder: Reminders) {
        cell.labelTitleField!.text = reminder.title ?? "New Reminder"
        cell.labelAlertField!.text = reminder.nextAlert!.description
    }

我正在使用 beginUpdates() 和 endUpdates() 来批量更新,以便如果一次有大量更改,操作系统可以找出处理更改的最佳方法并同时为所有动作设置动画。

// Batch the updates to the table. Start with beginUpdates so all the action animations are queued up.
    func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
        tableView.beginUpdates()
    }
// Batch the updates to the table. End with endUpdates to trigger the actual animations.
    func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
        tableView.endUpdates()
    }

这是控制台中与该代码相关的内容:

2019-03-23 12:31:09.307801-0500 Scheduler[5711:2218287] RemindersViewContoller in viewDidLoad: Fetched records successfully.
Refreshing reminders!
2019-03-23 12:31:09.311755-0500 Scheduler[5711:2218287] Reminder 'Test Non-recurring Reminder' section updated to Inactive.
2019-03-23 12:31:09.313254-0500 Scheduler[5711:2218287] RemindersViewController: Move was triggered, now updating row in table. Old path was <NSIndexPath: 0x28078e480> {length = 2, path = 0 - 0} and new path is <NSIndexPath: 0x28078f140> {length = 2, path = 1 - 2}
Scheduler was compiled with optimization - stepping may behave oddly; variables may not be available.
(lldb) 

所以看起来操作系统本身已经将提醒移动到它所属的位置,不知何故没有使用我的控制器功能。这必须在刷新各个提醒字段时发生,但是当我将上下文保存到核心数据时,它会再次调用我的控制器函数来执行已经完成的移动。

预期和实际结果

我想我已经在上面介绍了这一点,但总的来说,我想知道在表格视图显示之前更改获取的数据的最佳方法是什么,以便它全部位于正确的部分中,并相对于当前日期和时间进行排序。

更新

这是 refreshReminder 例程,只要注释掉保存到核心数据,它就可以完美运行。在我执行提醒记录的初始提取后立即调用此函数:

// Handle updating nextAlert and section based on current date and time
func refreshReminders () {

        // Loop through the records and update the time-sensitive section and nextAlert fields
        print("Refreshing reminders!")
        for reminder in fetchedController.fetchedObjects! {
            if (reminder.nextAlert! < Date()) {
                if (reminder.recurrence != "Never") {
                    let nextAlert = nextAlertDate(alertDate: reminder.alert!, recurrencePattern: reminder.recurrence)
                    if (reminder.nextAlert != nextAlert) {
                        reminder.nextAlert = nextAlert
                        os_log("Reminder '%{public}@' nextAlert date updated to %{public}@.", reminder.title!, String(describing: reminder.nextAlert!))
                    }
                }
                let section = getSection(nextAlertDate: reminder.nextAlert!)
                if (reminder.section != section) {
                    reminder.section = section
                    //print("Reminder section updated to", reminder.section!)
                    os_log("Reminder '%{public}@' section updated to %{public}@.", reminder.title!, reminder.section!)
                }
            }
        } //endfor

        // Save changes to core data if there are any
        /*if context.hasChanges {
             do {
             try context.save()
                 print("RemindersViewController in refreshReminders: Changes to core data, so saving them now.")
             } catch {
                 // Replace this implementation with code to handle the error appropriately.
                 // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
                 let nserror = error as NSError
                 fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
             } //enddo
         }*/ //endif

    } //endfunc refreshReminders

这是它在初始应用启动时更新/将提醒从一个部分移动到另一个的结果,因为它的提醒时间已经过去:

2019-03-23 18:26:24.796406-0500 Scheduler[6383:2371662] AppDelegate: Initialized core data stack
2019-03-23 18:26:24.801211-0500 Scheduler[6383:2371734] AppDelegate: Notification authorization granted.
2019-03-23 18:26:24.806165-0500 Scheduler[6383:2371734] AppDelegate: Set our custom notification categories and actions.
2019-03-23 18:26:24.810726-0500 Scheduler[6383:2371662] RemindersViewContoller in viewDidLoad: Fetched records successfully.
Refreshing reminders!
2019-03-23 18:26:24.814331-0500 Scheduler[6383:2371662] Reminder 'Test Non-recurring Reminder' section updated to Inactive.
Number of sections: 2
2019-03-23 18:26:24.815463-0500 Scheduler[6383:2371662] RemindersViewController in viewWillAppear: We're here. Let's see how often we get triggered!
Number of sections: 2
Number of records in section 1 : 3
Number of records in section 0 : 7
Number of sections: 2
Number of records in section 1 : 3
Number of records in section 0 : 7
2019-03-23 18:26:24.893329-0500 Scheduler[6383:2371662] RemindersViewController: Move was triggered, now updating row in table. Old path was <NSIndexPath: 0x28047a900> {length = 2, path = 0 - 0} and new path is <NSIndexPath: 0x28047a920> {length = 2, path = 1 - 3}
2019-03-23 18:26:24.893498-0500 Scheduler[6383:2371662] RemindersViewController: updated moved cell.
2019-03-23 18:26:24.893511-0500 Scheduler[6383:2371662] RemindersViewController: Moving row in table.
2019-03-23 18:26:24.893524-0500 Scheduler[6383:2371662] RemindersViewController: row moved.
Number of sections: 2
Number of sections: 2
Number of records in section 0 : 6
Number of records in section 1 : 4

这是我取消注释保存到核心数据时的 os_log 和错误。它在 configCell 上崩溃。

2019-03-23 18:35:22.210467-0500 Scheduler[6396:2375020] AppDelegate: Initialized core data stack
2019-03-23 18:35:22.215964-0500 Scheduler[6396:2375092] AppDelegate: Notification authorization granted.
2019-03-23 18:35:22.220937-0500 Scheduler[6396:2375092] AppDelegate: Set our custom notification categories and actions.
2019-03-23 18:35:22.227273-0500 Scheduler[6396:2375020] RemindersViewContoller in viewDidLoad: Fetched records successfully.
Refreshing reminders!
2019-03-23 18:35:22.230832-0500 Scheduler[6396:2375020] Reminder 'Test Non-recurring Reminder' section updated to Inactive.
2019-03-23 18:35:22.232326-0500 Scheduler[6396:2375020] RemindersViewController: Move was triggered, now updating row in table. Old path was <NSIndexPath: 0x282e6d360> {length = 2, path = 0 - 0} and new path is <NSIndexPath: 0x282e6df60> {length = 2, path = 1 - 3}
Scheduler was compiled with optimization - stepping may behave oddly; variables may not be available.
(lldb) 
configureCell(tableView.cellForRow(at: oldPath) as! RemindersCell, withReminder: anObject as! Reminders)
Thread 1: EXC_BREAKPOINT (code=1, subcode=0x104048220)

标签: iosuitableviewcore-data

解决方案


事实证明,即使单元格本身为零,也可以发送移动行请求。PaulW11 表示这很可能是因为该单元格实际上还没有在屏幕上可见。所以处理这个问题的方法是当且仅当它不是 nil 时更新(配置)单元格,但只要我们有一个有效的 oldPath 和 newPath,就继续处理行移动。我更新后的代码效果很好:

case .move:
            // If we have an old and new path, proceed with moving and/or updating the cell
            if let oldPath = indexPath, let newPath = newIndexPath {

                if let cell = tableView.cellForRow(at: oldPath) as? RemindersCell, let reminder = anObject as? Reminders {
                    os_log("RemindersViewController: Move was triggered, so updating cell in table. Reminder is: %{public}@", log: .default, type: .info, reminder)
                    configureCell(cell, withReminder: reminder)
                    os_log("RemindersViewController: Updated moving cell.", log: .default, type: .info)
                } else {
                    os_log("RemindersViewController: Move triggered, but cell isn't yet visible so skipping updating the cell fields.")
                }

            // If we have an old and new path, then go ahead and move the cell
            os_log("RemindersViewController: Moving row in table.", log: .default, type: .info)
            tableView.moveRow(at: oldPath, to: newPath)
                os_log("RemindersViewController: Row moved from %{public}@ to %{public}@.", log: .default, type: .info, oldPath as CVarArg, newPath as CVarArg)

            // If old and new path invalid, then log that
            } else {
                os_log("RemindersViewController: Move triggered, but old and new path aren't filled, so ignoring it.", log: .default, type: .error)
            }
            break

推荐阅读