在后台删除的数据导致 UITableView 中的索引超出范围崩溃

Posted

技术标签:

【中文标题】在后台删除的数据导致 UITableView 中的索引超出范围崩溃【英文标题】:Data removed in background causes index out of range crash in UITableView 【发布时间】:2015-08-28 19:09:33 【问题描述】:

代码

var animals: Results<Animal> 
    didSet 
        self.tableView.reloadData()
    

问题

我有一个 UITableView 显示动物列表 我还有一个后台进程定期同步动物列表 显示 UITableView 的 UIViewController 通过成员变量跟踪动物。 如果在我查看列表时后台进程从 Realm DB 中删除了最后一个 Animal,animals 集合会更新,但如果我滚动到哪里,UITableView 不知道它丢失了一行删除的 Animal 本来是,我从 Realm 收到一个 Index of bounds 异常并且应用程序崩溃了。

尝试的解决方案

    我尝试将Results&lt;Animal&gt; 复制到内存中的[Animal] 并将UITableView 绑定到那个,因为Array 不会像Results&lt;Animal&gt; 那样自动刷新(没有办法转对吧?)。这绕过了索引越界异常,除了现在我收到一个访问已删除/无效对象上的属性的异常,因为当单元格读取已删除对象上的属性时,它必须返回到领域。不会工作。 然后我尝试收听 Realm 的通知和 .reloadData 相应的表。这可行,但会导致过多的表重新加载,并增加确保通知令牌被释放等的开销,并记住在应用程序中的所有表视图中执行此操作。

解决方法

我最终创建了一个 RealmTableHandler 单例,它基本上是解决方案 #2 的升级版。

public extension UITableView 
    public func addToRealmTableHandler() 
        RealmTableHandler.sharedInstance.addTrackedTable(self)
    


/// Listens for Realm Notifications and reloads tracked UITableViews if needed
public class RealmTableHandler 
    public static let sharedInstance = RealmTableHandler()

    private var notificationToken: NotificationToken!

    private var tableIdentifier = 0
    private lazy var trackedTables = NSMapTable(keyOptions: NSHashTableWeakMemory, valueOptions: NSHashTableWeakMemory)
    private lazy var trackedDelegates = NSMapTable(keyOptions: NSHashTableWeakMemory, valueOptions: NSHashTableWeakMemory)
    private lazy var trackedDatasources = NSMapTable(keyOptions: NSHashTableWeakMemory, valueOptions: NSHashTableWeakMemory)

    private init() 
        self.startListening()
    

    private func startListening() 
        self.notificationToken = Realm().addNotificationBlock  (notification, realm) -> Void in
            self.checkTrackedTables()
        
    

    public func addTrackedTable(table: UITableView) 
        if self.trackedTables.count == 0 
            self.tableIdentifier = 0
        

        if let delegate = table.delegate, datasource = table.dataSource 
            self.tableIdentifier++

            self.trackedTables.setObject(table, forKey: self.tableIdentifier)
            self.trackedDelegates.setObject(delegate, forKey: self.tableIdentifier)
            self.trackedDatasources.setObject(datasource, forKey: self.tableIdentifier)
        
    

    private func checkTrackedTables() 
        // Ensure table, delegate, datasource are all available, sometimes the delegate or datasource are deallocated before the table, but the table keeps a reference to them
        for key in self.trackedTables.keyEnumerator().allObjects 
            if let table = self.trackedTables.objectForKey(key) as? UITableView,
                delegate = self.trackedDelegates.objectForKey(key) as? UITableViewDelegate,
                datasource = self.trackedDatasources.objectForKey(key) as? UITableViewDataSource
            
                // Check if tracked tables need to be reloaded due to Realm data changes
                let numSectionsInTableView = table.numberOfSections()
                var numRowsInTableView = 0
                let numSectionsInDatasource = datasource.numberOfSectionsInTableView?(table) ?? numSectionsInTableView
                var numRowsInDatasource = 0

                if numSectionsInTableView <= 0 || numSectionsInDatasource <= 0 
                    return
                

                for i in 0...(numSectionsInTableView - 1) 
                    numRowsInTableView += table.numberOfRowsInSection(i)
                
                for i in 0...(numSectionsInDatasource - 1) 
                    numRowsInDatasource += datasource.tableView(table, numberOfRowsInSection: i)
                

                if numRowsInTableView != numRowsInDatasource 
                    table.reloadData()
                
            
        
    

然后我将以下代码添加到任何UITableViews 我想在 Realm 增加或减少其行数时重新加载。

self.tableView.addToRealmTableHandler()

这似乎工作正常,即使使用分段 UITableViews(只要分段数据也是 Results&lt;T&gt;)。

有没有更好/更简单的方法来解决这个问题?

Realm 将来会支持表插入/删除吗?

【问题讨论】:

您可以使用 reloadRowsAtIndexPaths、addRowsAtIndexPaths、removeRowsAtIndexPaths、deleteRowsAtIndexPaths 添加、删除、移动或重新加载单行,而不是 ReloadData。 是的。不过,我想要一个全球通用的解决方案。它不知道哪些行索引受到影响。 【参考方案1】:

(免责声明:我为 Realm 工作。)

嗯,听起来您想要类似于 Core Data 中的 NSFetchedResultsController 的东西,当添加/删除对象时,您会在其中获得通用级别但细粒度的通知集。

我不确定这是否能解决您的问题,但实际上有一个开源库旨在使用名为 RBQFetchedResultsController (https://github.com/Roobiq/RBQFetchedResultsController) 的 Realm 来实现这一点。至少,这应该可以帮助您避免尝试自己创建自定义解决方案。 :)

如果这能解决您的问题,请告诉我!

【讨论】:

感谢您的推荐。不幸的是,我们没有时间进行研发,所以我们现在必须解决问题。不过,我会在某个时候调查一下!你们是否正在努力在 Realm 中对此提供本地支持? 别担心!是的!我们正在开发一个原生集成的解决方案,但我们还没有预计的发布日期(因此我在这里没有提到它)。

以上是关于在后台删除的数据导致 UITableView 中的索引超出范围崩溃的主要内容,如果未能解决你的问题,请参考以下文章

从 UITableView 中删除值会导致崩溃

如何在 Swift 中从 Core Data 中删除数据时删除 UITableView 行?

删除核心数据对象并保存在后台线程中

关于UITableview刷新笔记

从 UITableView 中删除行会导致部分标题问题?

在uitableview单元格上留下滑动删除按钮打开然后返回导致崩溃