NSDiffableDataSourceSnapshot "reloadItems" ne içindir?

Sep 26 2020

NSDiffableDataSourceSnapshot'ın kullanımını bulmakta güçlük çekiyorum reloadItems(_:):

  • Yeniden yüklemeyi istediğim öğe, veri kaynağında zaten mevcut olan bir öğeyle eşit değilse, çökerim.

  • Ancak öğe , veri kaynağında zaten mevcut olan bir öğeye eşitse, onu "yeniden yüklemenin" amacı nedir?

İkinci noktanın cevabının şu olduğunu düşünebilirsiniz: öğe tanımlayıcı nesnenin, eşitlenebilirliğinin bir parçası olmayan ancak hücre arayüzüne yansıyan başka bir yönü olabilir. Ama bulduğum şey bunun doğru olmadığı; çağırdıktan sonra reloadItems, tablo görünümü yok değil değişikliği yansıtır.

Bu nedenle, bir öğeyi değiştirmek istediğimde, anlık görüntü ile yaptığım şey, insertdeğiştirilecek öğeden sonra ve ardından deleteorijinal öğeden sonra . Anlık görüntü replaceyöntemi yok , öyle olmasını umuyordum reloadItems.

(Bu terimlerle ilgili bir Yığın Taşması araması yaptım ve çok az şey buldum - çoğunlukla reloadItems, farklı UITableView kullanarak bir tablo hücresi nasıl güncellenir gibi belirli kullanımları konusunda kafa karıştıran birkaç soru . ne pratik kullanım vardır herkes bu yöntem bulundu?)


Oynamak için minimum tekrarlanabilir bir örneğe sahip olmak gibisi yoktur, işte bir tane.

ViewController şablonuyla sade bir iOS projesi oluşturun ve bu kodu ViewController'a ekleyin.

Parça parça alacağım. İlk olarak, öğe tanımlayıcımız olarak hizmet edecek bir yapımız var. UUID benzersiz bir parçadır, bu nedenle eşitlik ve karmalık yalnızca ona bağlıdır:

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

Ardından, (sahte) tablo görünümü ve ayrılabilir veri kaynağı:

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

Yani, ayrılabilir veri kaynağımızda sadece bir UniBool var ve o boolda true. Şimdi, booldeğeri değiştirmeye çalışan bu eylem yöntemini çağırmak için bir düğme ayarlayın 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)
        }
    }
}

İşte olay şu. Ben söylenen reloadItemskimin UUID bir maç, ama kimin bir öğe ile boolkonum değiştirir: "Bu nesnenin ison yanlıştır". Ama enstantaneyi sorduğumda, tamam, elinizde ne var? bana, tek öğe tanımlayıcısının bool hala doğru olduğunu söylüyor .

Ve o ben soruyorum budur. Enstantane yeni değerini almayacaksa , ilk olarak boolne işe yarar reloadItems?

Açıkçası ben sadece farklı bir UniBool, yani farklı bir UUID ile değiştirebilirim. Ama o zaman arayamam reloadItems; çöküyoruz çünkü bu UniBool zaten verilerin içinde değil. Bunu inserttakiben arayarak çözebilirim removeve işte tam da bu şekilde çalışıyorum.

Ama sorum şu: Öyleyse reloadItemsbu şey için değilse ne için?

Yanıtlar

2 Paulw11 Sep 27 2020 at 21:22

Yeni örnek kodunuza göre, kabul ediyorum, bir hata gibi görünüyor. Bir reloadItemsanlık görüntüye bir eklediğinizde , veri kaynağı kapanışını güncellenmiş bir hücre talep etmek için doğru şekilde tetikler, ancak IdentifierTypekapanışa iletilen öğe, reloadItemsçağrıyla sağlanan yeni değer değil, orijinaldir .

UniBoolYapınızı bir değer türü yerine bir başvuru olacak şekilde bir sınıfa değiştirirsem, işler beklendiği gibi çalıştı (çünkü artık UniBoolaynı tanımlayıcıya sahip yeni bir tane yerine tek bir örnek var ).

Görünüşe göre şu anda birkaç olası çözüm var:

  1. İçin bir değer türü yerine bir başvuru kullanın IdentifierType
  2. Dizi gibi ek bir destek deposu kullanın indexPathve veri kaynağı kapanışından ona erişin .

İkisinin de ideal olduğunu düşünmüyorum.

İlginç bir şekilde, UniBoolbir sınıfa geçtikten sonra , mevcut örnekle UniBoolaynı olan yeni bir örneğini oluşturmayı uuidve onu yeniden yüklemeyi denedim ; Kod , yeniden yükleme için geçersiz öğe tanımlayıcısı belirtilmiş bir istisna ile çöktü ; Bu bana doğru gelmiyor; hashValueGerçek nesne referansı değil, yalnızca önemli olmalıdır. Hem orijinal hem de yeni nesneler aynıydı hashValueve ==geri döndü true.


Orijinal cevap

reloadItems işe yarıyor, ancak iki önemli nokta var:

  1. Veri kaynağının akımıyla başlamalı snapshotve onu aramalısınız reloadItems. Yeni bir anlık görüntü oluşturamazsınız.

  2. Sen güvenemez itemgeçirilen CellProviderkapatılması dışında herhangi bir amaçlaidentifier - Bu senin sırt modeli (dizi) en son verileri temsil etmez.

Nokta 2, sağlanan nesneyi kullanmanız indexPathveya item.idgüncellenmiş nesnenizi modelinizden almanız gerektiği anlamına gelir .

Bir tablo satırında geçerli saati gösteren basit bir örnek oluşturdum ; Bu veri kaynağı yapısıdır:

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

hashYalnızca idözelliği kullanan işleve rağmen, geçersiz kılmanın da gerekli olduğunu ==veya satırı yeniden yüklemeye çalıştığınızda geçersiz bir tanımlayıcıyla bir çökme yaşayacağınızı unutmayın.

Her saniye rastgele seçilmiş satırlar yeniden yüklenir. Kodu çalıştırdığınızda, rastgele seçilen satırlarda zamanın güncellendiğini görürsünüz.

Bu, aşağıdakileri kullanan koddur 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

Farkında olmadan aynı soruyu yayınladım . Bunu öncelikle modelimi sınıflara dönüştürerek çalıştırdım. Ardından, 'reloadItems' çağrıldıktan sonra 'applySnapshot' çağırılır.

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

(Soruda gösterilen davranışla ilgili bir hata yaptım, çünkü bunun iyi bir davranış olduğunu düşünmüyorum. Ancak, her durumda, fikrin ne olması gerektiği konusunda bir tahmin sunabileceğimi düşünüyorum.)


Eğer bir anlık anlatınca reloadbelli bir öğe, bu yok değil sağladığınız öğenin verileri okumak! Bu sadece görünen tanımlamanın bir yolu olarak, öğenin de neyi , öğeyi zaten veri kaynağında, sen yeniden yükleme için soruyorsunuz.

(Bu nedenle, sağladığınız öğe Eşit olabilir ancak zaten veri kaynağında bulunan öğeyle% 100 aynı değilse, sağladığınız öğe ile veri kaynağında bulunan öğe arasındaki "fark" hiç önemli olmayacaktır ; veriler kaynağa hiçbir şeyin farklı olduğu asla söylenmeyecek.)

Daha sonra applyveri kaynağına giden bu anlık görüntüyü aldığınızda , veri kaynağı tablo görünümüne karşılık gelen hücreyi yeniden yüklemesini söyler. Bu, veri kaynağının hücre sağlayıcı işlevinin yeniden çağrılmasına neden olur.

Tamam, bu nedenle veri kaynağının hücre sağlayıcısı işlevi, olağan üç parametre ile çağrılır - tablo görünümü, dizin yolu ve veri kaynağından gelen veriler. Ancak veri kaynağındaki verilerin değişmediğini söylemiştik. Öyleyse yeniden yüklemenin amacı nedir?

Cevap, görünüşe göre, hücre sağlayıcı işlevinin, yeni kuyruğu alınmış hücrede görüntülenecek yeni verileri (en azından bir kısmını) elde etmek için başka bir yere bakması beklenir . Hücre sağlayıcısının baktığı bir tür "destek deposuna" sahip olmanız beklenir. Örneğin, anahtarın hücre tanımlayıcı türü ve değerin yeniden yüklenebilecek ekstra bilgiler olduğu bir sözlüğü tutuyor olabilirsiniz.

Bu yasal olmalıdır, çünkü tanım gereği hücre tanımlayıcı türü Karıştırılabilirdir ve bu nedenle bir sözlük anahtarı olarak hizmet edebilir ve ayrıca hücre tanımlayıcılarının veri içinde benzersiz olması gerekir, aksi takdirde veri kaynağı verileri reddedebilir (kilitlenerek). Ve arama anında yapılacaktır çünkü bu bir sözlüktür.


İşte kopyalayıp doğrudan bir projeye yapıştırabileceğiniz eksiksiz bir çalışma örneği. Tablo, favori veya favori olmadığını belirten, yıldızın doldurulması veya boş bırakılması için kullanıcının dokunabileceği bir yıldızla birlikte üç adı tasvir eder. İsimler farklı veri kaynağında saklanır, ancak favori durumu harici destek deposunda saklanır.

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