NSDiffableDataSourceSnapshot `reloadItems` คืออะไร?

Sep 26 2020

ฉันมีปัญหาในการค้นหาการใช้ NSDiffableDataSourceSnapshot reloadItems(_:):

  • หากรายการที่ฉันขอให้โหลดซ้ำไม่สามารถเทียบได้กับรายการที่มีอยู่แล้วในแหล่งข้อมูลฉันจะขัดข้อง

  • แต่ถ้ารายการนั้นเทียบเท่ากับรายการที่มีอยู่แล้วในแหล่งข้อมูลแล้ว "การโหลดซ้ำ" จะเป็นอย่างไร

คุณอาจคิดว่าคำตอบของประเด็นที่สองคืออาจมีลักษณะอื่น ๆ ของอ็อบเจ็กต์ตัวระบุไอเท็มที่ไม่ได้เป็นส่วนหนึ่งของความเท่าเทียมกัน แต่สะท้อนไปยังส่วนต่อประสานเซลล์ แต่สิ่งที่ฉันพบคือมันไม่จริง หลังจากโทรreloadItemsแล้วมุมมองตารางจะไม่แสดงถึงการเปลี่ยนแปลง

ดังนั้นเมื่อฉันต้องการเปลี่ยนรายการสิ่งที่ฉันทำกับสแน็ปช็อตคือinsertหลังจากที่รายการที่จะถูกแทนที่แล้วเป็นdeleteของรายการเดิม ไม่มีreplaceวิธีการสแนปชอตซึ่งเป็นสิ่งที่ฉันหวังว่าreloadItemsจะเป็น

(ฉันทำการค้นหา Stack Overflow ในคำเหล่านั้นและพบน้อยมาก - ส่วนใหญ่เป็นเพียงคำถามสองสามข้อที่ทำให้งงงวยกับการใช้งานบางอย่างreloadItemsเช่นวิธีอัปเดตเซลล์ตารางโดยใช้ 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 ในแหล่งข้อมูล diffable ของเราและมี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

จากตัวอย่างโค้ดใหม่ของคุณฉันยอมรับดูเหมือนว่ามีข้อบกพร่อง เมื่อคุณเพิ่ม a 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)
    }
}