Для чего нужен NSDiffableDataSourceSnapshot `reloadItems`?

Sep 26 2020

Мне трудно найти использование NSDiffableDataSourceSnapshot reloadItems(_:):

  • Если элемент, который я прошу перезагрузить, не соответствует элементу, который уже присутствует в источнике данных, я выхожу из строя.

  • Но если элемент является equatable к элементу , который уже присутствует в источнике данных, то в чем смысл «перезагрузки» это?

Вы можете подумать, что ответ на второй вопрос таков: что ж, может быть какой-то другой аспект объекта идентификатора элемента, который не является частью его эквивалентности, но отражается в интерфейсе ячейки. Но я обнаружил, что это неправда; после вызова reloadItemsв табличном представлении изменения не отражаются.

Поэтому, когда я хочу изменить элемент, то, что я делаю со снимком, - это insertпосле заменяемого элемента, а затем deleteпосле исходного элемента. Не существует replaceметода моментальных снимков , как я надеялся reloadItems.

(Я провел поиск по этим терминам и нашел очень мало - в основном всего пару вопросов, которые озадачили конкретное использование reloadItems, например, Как обновить ячейку таблицы с помощью diffable UITableView . Поэтому я спрашиваю в более обобщенной форме , какое практическое применение нашел кто-нибудь в этом методе?)


Что ж, нет ничего лучше, чем иметь минимально воспроизводимый пример, с которым можно поиграть, так что вот один.

Создайте простой проект iOS с его шаблоном ViewController и добавьте этот код в ViewController.

Я возьму это по частям. Во-первых, у нас есть структура, которая будет служить идентификатором нашего элемента. UUID - это уникальная часть, поэтому равноправие и хешируемость зависят только от него:

struct UniBool : Hashable {
    let uuid : UUID
    var bool : Bool
    // equatability and hashability agree, only the UUID matters
    func hash(into hasher: inout Hasher) {
        hasher.combine(uuid)
    }
    static func ==(lhs:Self, rhs:Self) -> Bool {
        lhs.uuid == rhs.uuid
    }
}

Затем (поддельное) табличное представление и источник данных для различий:

let tableView = UITableView(frame: .zero, style: .plain)
var datasource : UITableViewDiffableDataSource<String,UniBool>!
override func viewDidLoad() {
    super.viewDidLoad()
    self.tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
    self.datasource = UITableViewDiffableDataSource<String,UniBool>(tableView: self.tableView) { tv, ip, isOn in
        let cell = tv.dequeueReusableCell(withIdentifier: "cell", for: ip)
        return cell
    }
    var snap = NSDiffableDataSourceSnapshot<String,UniBool>()
    snap.appendSections(["Dummy"])
    snap.appendItems([UniBool(uuid: UUID(), bool: true)])
    self.datasource.apply(snap, animatingDifferences: false)
}

Итак, в нашем источнике данных для распространения есть только один UniBool, и он boolесть true. Итак, теперь настройте кнопку для вызова этого метода действия, который пытается переключить boolзначение, используя reloadItems:

@IBAction func testReload() {
    if let unibool = self.datasource.itemIdentifier(for: IndexPath(row: 0, section: 0)) {
        var snap = self.datasource.snapshot()
        var unibool = unibool
        unibool.bool = !unibool.bool
        snap.reloadItems([unibool]) // this is the key line I'm trying to test!
        print("this object's isOn is", unibool.bool)
        print("but looking right at the snapshot, isOn is", snap.itemIdentifiers[0].bool)
        delay(0.3) {
            self.datasource.apply(snap, animatingDifferences: false)
        }
    }
}

Так вот в чем дело. Я сказал для reloadItemsэлемента, чей UUID совпадает, но boolкоторый переключается: «isON этого объекта является ложным». Но когда я спрашиваю снимок, хорошо, а что у вас? он сообщает мне, что его единственный идентификатор элемента все bool еще верен .

И это то, о чем я спрашиваю. Если снимок не получит новое значение bool, для чего reloadItemsв первую очередь?

Очевидно, я мог бы просто заменить другой UniBool, то есть с другим UUID. Но тогда я не могу позвонить reloadItems; происходит сбой, потому что этого UniBool еще нет в данных. Я могу обойти это, позвонив с insertпоследующим remove, и это именно то, как я работаю.

Но у меня вопрос: а для чего reloadItems, как не для этого?

Ответы

2 Paulw11 Sep 27 2020 at 21:22

На основании вашего нового примера кода, я согласен, это похоже на ошибку. Когда вы добавляете reloadItemsв моментальный снимок, он правильно запускает закрытие источника данных для запроса обновленной ячейки, но IdentifierTypeэлемент, который передается в закрытие, является исходным, а не новым значением, которое было предоставлено с reloadItemsвызовом.

Если я изменил вашу UniBoolструктуру на класс, чтобы она была ссылкой, а не типом значения, тогда все работало так, как ожидалось (поскольку теперь существует единственный экземпляр, UniBoolа не новый с тем же идентификатором).

Кажется, на данный момент есть несколько возможных обходных путей:

  1. Используйте ссылку, а не тип значения для IdentifierType
  2. Используйте дополнительное хранилище резервных копий, например массив, и обращайтесь к нему через indexPathзакрытие источника данных.

Я не думаю, что это идеальный вариант.

Интересно, что после того, как я перешел UniBoolна класс, я попытался создать новый экземпляр, UniBoolкоторый имел то же самое, uuidчто и существующий, и перезагрузил его; Произошел сбой кода, возникла исключительная ситуация, в которой указан недопустимый идентификатор элемента для перезагрузки ; Мне это не кажется правильным; Только то, что hashValueдолжно иметь значение, а не фактическая ссылка на объект. И исходный, и новый объекты были одинаковы hashValueи ==возвращены true.


Оригинальный ответ

reloadItems работает, но есть два важных момента:

  1. Вы должны начать с текущего источника данных snapshotи обратиться reloadItemsк нему. Вы не можете создать новый снимок.

  2. Вы не можете полагаться на itemпереданное в CellProviderзакрытие ни для чего другого, кромеidentifier - Он не представляет самые последние данные из вашей резервной модели (массива).

Пункт 2 означает, что вам нужно использовать предоставленный объект indexPathили item.idполучить обновленный объект из вашей модели.

Я создал простой пример , отображающий текущее время в строке таблицы; Это структура источника данных:

struct RowData: Hashable {
    var id: UUID = UUID()
    var name: String
    private let possibleColors: [UIColor] = [.yellow,.orange,.cyan]
    var timeStamp = Date()
    
    func hash(into hasher: inout Hasher) {
        hasher.combine(self.id)
    }
    
    static func ==(lhs: RowData, rhs: RowData) -> Bool {
        return lhs.id == rhs.id
    }
}

Обратите внимание, что, несмотря на то, что hashфункция использует только idсвойство, также необходимо переопределить, ==иначе вы получите сбой с недопустимым идентификатором при попытке перезагрузить строку.

Каждую секунду перезагружается случайный выбор строк. Когда вы запускаете код, вы видите, что время обновляется в этих случайно выбранных строках.

Это код, который использует reloadItems:

self.timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { (timer) in
    guard let datasource = self.tableview.dataSource as? UITableViewDiffableDataSource<Section,RowData> else {
        return
    }
    var snapshot = datasource.snapshot()
    var rowIdentifers = Set<RowData>()
    for _ in 0...Int.random(in: 1...self.arrItems.count) {
        let randomIndex = Int.random(in: 0...self.arrItems.count-1)
        self.arrItems[randomIndex].timeStamp = Date()
        rowIdentifers.insert(self.arrItems[randomIndex])
    }

    snapshot.reloadItems(Array(rowIdentifers))
    datasource.apply(snapshot)
}
1 user9400730 Nov 12 2020 at 11:53

Я разместил тот же вопрос , не осознавая. Я получил эту работу, сначала преобразовав мою модель в классы. Затем вызываем applySnapshot после вызова reloadItems.

func toggleSelectedStateForItem(at indexPath: IndexPath, animate: Bool = true) {
    let item = dataSource.itemIdentifier(for: indexPath)!
    var snapshot = dataSource.snapshot()
    item.isSelected = !item.isSelected
    snapshot.reloadItems([item])
    dataSource.apply(snapshot)
}
matt Oct 01 2020 at 23:41

(Я зарегистрировал ошибку в поведении, продемонстрированном в вопросе, потому что я не считаю это хорошим поведением. Но в сложившейся ситуации я думаю, что могу предположить, в чем заключается идея.)


Когда вы передаете снимок reloadопределенному элементу, он не считывает данные предоставленного вами элемента! Он просто смотрит на элемент, чтобы определить, какой элемент, уже находящийся в источнике данных, вы просите перезагрузить.

(Таким образом, если поставляемый вами элемент равен, но не на 100% идентичен элементу, уже находящемуся в источнике данных, «разница» между предоставленным вами элементом и элементом, уже содержащимся в источнике данных, не будет иметь никакого значения ; данные источнику никогда не сообщат, что что-то другое.)

Когда вы затем делаете applyэтот снимок в источник данных, источник данных сообщает табличному представлению о необходимости перезагрузки соответствующей ячейки. Это приводит к повторному вызову функции поставщика ячеек источника данных .

Итак, вызывается функция поставщика ячеек источника данных с тремя обычными параметрами - табличным представлением, путем индекса и данными из источника данных. Но мы только что сказали, что данные из источника данных не изменились. Так какой вообще смысл перезагружать?

Ответ, по-видимому, заключается в том, что функция провайдера ячейки должна искать в другом месте, чтобы получить (по крайней мере, некоторые из) новые данные, которые будут отображаться во вновь удаленной ячейке. Ожидается, что у вас будет своего рода «резервное хранилище», на которое смотрит поставщик сотовой связи. Например, вы можете поддерживать словарь, в котором ключом является тип идентификатора ячейки, а значением - дополнительная информация, которая может быть перезагружена.

Это должно быть законным, потому что по определению тип идентификатора ячейки является Hashable и, следовательно, может служить ключом словаря, и, кроме того, идентификаторы ячеек должны быть уникальными в пределах данных, иначе источник данных отклонит данные (из-за сбоя). И поиск будет мгновенным, потому что это словарь.


Вот полный рабочий пример, который вы можете просто скопировать и вставить прямо в проект. В таблице изображены три имени и звездочка, которую пользователь может нажать, чтобы сделать звезду заполненной или пустой, что указывает на избранное или не избранное. Имена хранятся в источнике данных для различения, но статус избранного хранится во внешнем резервном хранилище.

extension UIResponder {
    func next<T:UIResponder>(ofType: T.Type) -> T? {
        let r = self.next
        if let r = r as? T ?? r?.next(ofType: T.self) {
            return r
        } else {
            return nil
        }
    }
}
class TableViewController: UITableViewController {
    var backingStore = [String:Bool]()
    var datasource : UITableViewDiffableDataSource<String,String>!
    override func viewDidLoad() {
        super.viewDidLoad()
        let cellID = "cell"
        self.tableView.register(UITableViewCell.self, forCellReuseIdentifier: cellID)
        self.datasource = UITableViewDiffableDataSource<String,String>(tableView:self.tableView) {
            tableView, indexPath, name in
            let cell = tableView.dequeueReusableCell(withIdentifier: cellID, for: indexPath)
            var config = cell.defaultContentConfiguration()
            config.text = name
            cell.contentConfiguration = config
            var accImageView = cell.accessoryView as? UIImageView
            if accImageView == nil {
                let iv = UIImageView()
                iv.isUserInteractionEnabled = true
                let tap = UITapGestureRecognizer(target: self, action: #selector(self.starTapped))
                iv.addGestureRecognizer(tap)
                cell.accessoryView = iv
                accImageView = iv
            }
            let starred = self.backingStore[name, default:false]
            accImageView?.image = UIImage(systemName: starred ? "star.fill" : "star")
            accImageView?.sizeToFit()
            return cell
        }
        var snap = NSDiffableDataSourceSnapshot<String,String>()
        snap.appendSections(["Dummy"])
        let names = ["Manny", "Moe", "Jack"]
        snap.appendItems(names)
        self.datasource.apply(snap, animatingDifferences: false)
        names.forEach {
            self.backingStore[$0] = false
        }
    }
    @objc func starTapped(_ gr:UIGestureRecognizer) {
        guard let cell = gr.view?.next(ofType: UITableViewCell.self) else {return}
        guard let ip = self.tableView.indexPath(for: cell) else {return}
        guard let name = self.datasource.itemIdentifier(for: ip) else {return}
        guard let isFavorite = self.backingStore[name] else {return}
        self.backingStore[name] = !isFavorite
        var snap = self.datasource.snapshot()
        snap.reloadItems([name])
        self.datasource.apply(snap, animatingDifferences: false)
    }
}