【问题标题】:Updating UITableView with multiple sections from RLMResults.Observe()使用 RLMResults.Observe() 中的多个部分更新 UITableView
【发布时间】:2017-11-22 09:44:28
【问题描述】:

我正在尝试创建一个自动更新的 TableView,这通常在 Results.observe 的帮助下很容易做到(替换 .addNotificationBlock)

我面临的问题是我无法弄清楚如何处理具有多个部分的 tableView,以及可以从一个部分移动到另一个部分的单元格。

以下表为例:(见UITableView with Multiple Sections using Realm and Swift

斗牛犬

查理

最大

德国牧羊犬

贝拉

朋友

莫莉

金毛寻回犬

贝利

西伯利亚哈士奇

菊花

class Dog: Object {
@objc dynamic var name String?
@objc dynamic var race: String?
}

然后是类似的东西:

let results = realm.objects(Dog.self)
    let token = dogs.observe { changes in
        switch changes {
        case .initial(let dogs):
             break
         case .update:
         // HANDLE MOVING CELL TO DIFFERENT SECTION HERE
             break
         case .error:
             break
         }
     }

假设我有上面的 tableView,但 'Molly' 遇到了身份危机,结果证明是一只金毛猎犬,所以我们从细节屏幕中更改了比赛。

我将如何处理 Observe 块中的这种变化?

我尝试使用 1 个结果列表 / 令牌,当我们更改比赛属性时会触发修改。但是除了一个完整的 reloadData(),因为我需要动画,我无法使用它,我无法弄清楚如何在 2 个不同的部分处理删除和插入,因为我们无法访问“狗”中的先前数据'-目的。因此,我不知道如何确定一个单元格是否应该移动到不同的部分以及前一部分是什么。

我也尝试使用每个部分的结果列表,但这会导致不一致。当我更改 race 属性时,它会触发修改(狗对象已更改)、删除(上一节的 resultList.count 为 -1)和插入(新节的 resultList.count = +1 )。这些通知不会同时触发,从而导致错误:

'尝试从第x节中删除项目x,但只有x 更新前的部分'

有没有人想出如何优雅地处理这个问题?在我正在实习的项目中,我实际上需要在多个 tableView 中使用类似的东西。

提前致谢

(第一次发帖,如有不合标准请及时指正)

----- 使用更具体的示例代码进行编辑 -----

我正在使用的数据类删除了一些不重要的属性

    class CountInfo: Object, Encodable {
                    @objc dynamic var uuid: String?
                    @objc dynamic var productName: String?
// TableView is split in 2 sections based on this boolean-value  
                    @objc dynamic var inStock: Bool = false 
                }

viewDidLoad() 中的代码存根我想用 2 个部分来更新我的 tableView

        self.countListProducts = realm.objects(CountInfo.self)
        self.token = self.countListProducts.observe {
            changes in
            AppDelegate.log.debug(changes)
            if let tableView = self.tableView {
                switch changes {
                case .initial:
                    // if countInfo.isCounted = true: insert in section 0, if false: insert in section 1
                    // Currently handled by cellForRowAt
                    tableView.reloadData()
                case .update(_, let deletions, let insertions, let modifications):

                    // Remove deletion rows from correct section
                    // Insert insertions into correct section
                    // Reload Cell if modification didn't change 'isCounted' property
                    // Remove from old section and insert in new section if 'isCounted' property changed


                    tableView.beginUpdates()
                    tableView.insertRows(at: insertions.map({ /* GET ROW TO INSERT */ }),
                                         with: .automatic)
                    tableView.deleteRows(at: deletions.map({ /* GET ROW TO DELETE */ }),
                                         with: .automatic)
                    tableView.reloadRows(at: modifications.map({ /* UPDATE NAME OR MOVE TO OTHER SECTION IF 'inStock' value Changed */ }),
                                         with: .automatic)
                    tableView.endUpdates()

                case .error(let error):
                    // An error occurred while opening the Realm file on the background worker thread
                    fatalError("\(error)")
                }
            }

【问题讨论】:

  • 您是否使用数组作为您的 tableView 数据源?当用户进行更改时,数据是在什么时候写入 Realm 的,您是手动更新数据源还是通过观察重新加载?
  • 我在 dog 类下面添加了一些代码,显示我希望如何处理更改。希望这能澄清我的意思
  • 因为更新通知没有告诉我更改了哪些属性。它也不会给我以前的种族属性,我需要弄清楚要删除哪一行,尽管我想我可以从 didSet 存储一个“上一节”值来解决这部分问题。
  • 哦!您的 Realm 对象通常会有一个 Primary Key,它是标识该领域对象的唯一属性。当通知触发时,该对象被传入(通过 insertions 数组),使用 .map 对其进行迭代,并使用每个对象的键来处理 dataSource 数组中的该对象,更新/相应地删除它并在 tableView 上执行插入/删除。

标签: swift realm


【解决方案1】:

这个问题困扰了我很长时间,但我终于想通了。我希望这对某人有所帮助。

当一个对象从一个结果集移动到另一个结果集时,您应该期待两个领域通知。一个用于旧结果集(删除),一个用于新结果集(插入)。

当我们收到第一个结果集的通知,然后更新该部分的 tableView 时,tableView 将为两个部分调用numberOfRowsInSection。当 tableView 发现其他部分的结果集发生了变化,但我们没有更新该部分的 tableView 时,它会抱怨NSInternalInconsistencyException

我们需要做的是让 tableView 认为其他部分没有更新。我们通过保持自己的计数来做到这一点。

基本上,您需要做一些事情。

  1. 为每个部分维护单独的结果集
  2. 为每个部分维护单独的通知处理程序
  3. 维护每个部分的手动对象计数
  4. 当通知处理程序触发删除或插入时更新对象计数
  5. 返回numberOfRowsInSection 中的手动对象计数(不是结果集计数)

这是我的模型对象:

class Contact: Object {

    @objc dynamic var uuid: String = UUID().uuidString
    @objc dynamic var firstName: String = ""
    @objc dynamic var lastName: String = ""
    @objc dynamic var age: Int = 0

    convenience init(firstName: String, lastName: String, age: Int) {
        self.init()
        self.firstName = firstName
        self.lastName = lastName
        self.age = age
    }

    override class func primaryKey() -> String? {
        return "uuid"
    }
}

在我这里的示例代码中有两个部分。第一个包含 70 岁以下的联系人列表,另一个包含 70 岁以上的联系人列表。通过维护我们在领域通知触发时手动更新的对象计数,我们能够从一个结果中移动对象在没有 UIKit 抱怨的情况下设置为下一个。

let elderAge = 70

lazy var allContacts: Results<Contact> = {
    let realm = try! Realm()
    return realm.objects(Contact.self)
}()

// KEEP A RESULT SET FOR SECTION 0

lazy var youngContacts: Results<Contact> = {
    return allContacts
        .filter("age <= %@", elderAge)
        .sorted(byKeyPath: "age", ascending: true)
}()

// KEEP A RESULT SET FOR SECTION 1

lazy var elderContacts: Results<Contact> = {
    return allContacts
        .filter("age > %@", elderAge)
        .sorted(byKeyPath: "age", ascending: true)
}()

// MANUALLY MAINTAIN A COUNT OF ALL OBJECTS IN SECTION 0

lazy var youngContactsCount: Int = {
    return youngContacts.count
}()

// MANUALLY MAINTAIN A COUNT OF ALL OBJECTS IN SECTION 1

lazy var elderContactsCount: Int = {
    return elderContacts.count
}()

// OBSERVE OBJECTS IN SECTION 0

lazy var youngToken: NotificationToken = {
    return youngContacts.observe { [weak self] change in
        guard let self = self else { return }
        switch change {
        case .update(_, let del, let ins, let mod):

            // MANUALLY UPDATE THE OBJECT COUNT FOR SECTION 0

            self.youngContactsCount -= del.count
            self.youngContactsCount += ins.count

            // REFRESH THE SECTION

            self.refresh(section: 0, del: del, ins: ins, mod: mod)
        default:
            break
        }
    }
}()

// OBSERVE OBJECTS IN SECTION 1

lazy var elderToken: NotificationToken = {
    return elderContacts.observe { [weak self] change in
        guard let self = self else { return }
        switch change {
        case .update(_, let del, let ins, let mod):

            // MANUALLY UPDATE THE OBJECT COUNT FOR SECTION 1

            self.elderContactsCount -= del.count
            self.elderContactsCount += ins.count

            // REFRESH THE SECTION

            self.refresh(section: 1, del: del, ins: ins, mod: mod)
        default:
            break
        }
    }
}()

func refresh(section: Int, del: [Int], ins: [Int], mod: [Int]) {
    tableView.beginUpdates()
    tableView.deleteRows(
        at: del.map { .init(row: $0, section: section) },
        with: .automatic
    )
    tableView.insertRows(
        at: ins.map { .init(row: $0, section: section) },
        with: .automatic
    )
    tableView.reloadRows(
        at: mod.map { .init(row: $0, section: section) },
        with: .automatic
    )
    tableView.endUpdates()
}


override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {

    // RETURN THE MANUALLY CALCULATED OBJECT COUNT (NOT THE ACTUAL RESULT SET COUNT)

    //return section == 0 ? youngContacts.count : elderContacts.count

    return section == 0 ? youngContactsCount : elderContactsCount
}

这是完整源文件的LINK

【讨论】:

  • 谢谢!相同的方法可以用于具有多个部分的表视图,每个部分都填充了来自不同核心数据获取结果控制器的数据。这是我的情况。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-02-15
  • 1970-01-01
  • 2012-11-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多