NSDiffableDataSourceSnapshot `reloadItems` คืออะไร?
ฉันมีปัญหาในการค้นหาการใช้ 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ถ้าไม่ใช่เพื่อสิ่งนี้คืออะไร?
คำตอบ
จากตัวอย่างโค้ดใหม่ของคุณฉันยอมรับดูเหมือนว่ามีข้อบกพร่อง เมื่อคุณเพิ่ม a reloadItemsลงในสแนปชอตมันจะทริกเกอร์การปิดแหล่งข้อมูลอย่างถูกต้องเพื่อขอเซลล์ที่อัปเดต แต่IdentifierTypeรายการที่ส่งผ่านไปยังการปิดนั้นเป็นค่าดั้งเดิมไม่ใช่ค่าใหม่ที่มาพร้อมกับการreloadItemsโทร
ถ้าฉันเปลี่ยนโครงสร้างของคุณUniBoolเป็นคลาสเพื่อให้เป็นการอ้างอิงแทนที่จะเป็นประเภทค่าสิ่งต่าง ๆ ก็ทำงานได้ตามที่คาดไว้ (เนื่องจากตอนนี้มีอินสแตนซ์เดียวUniBoolแทนที่จะเป็นแบบใหม่ที่มีตัวระบุเดียวกัน)
ดูเหมือนว่าในขณะนี้มีวิธีแก้ไขที่เป็นไปได้สองประการ:
- ใช้การอ้างอิงมากกว่าประเภทค่าสำหรับ
IdentifierType - ใช้ที่เก็บสำรองเพิ่มเติมเช่นอาร์เรย์และเข้าถึงผ่าน
indexPathในการปิดแหล่งข้อมูล
ฉันไม่คิดว่าสิ่งเหล่านี้เหมาะอย่างยิ่ง
ที่น่าสนใจคือหลังจากที่ฉันเปลี่ยนUniBoolเป็นคลาสฉันได้ลองสร้างอินสแตนซ์ใหม่UniBoolที่มีลักษณะเดียวuuidกับอินสแตนซ์ที่มีอยู่แล้วโหลดซ้ำ รหัสชนกับข้อยกเว้นที่ระบุตัวระบุรายการที่ระบุไม่ถูกต้องสำหรับโหลด ; สิ่งนี้ไม่ถูกต้องสำหรับฉัน สิ่งที่hashValueควรมีเท่านั้นไม่ใช่การอ้างอิงวัตถุจริง ทั้งต้นฉบับและวัตถุใหม่มีเหมือนกันhashValueและกลับ==true
คำตอบเดิม
reloadItems ใช้งานได้ แต่มีสองประเด็นสำคัญ:
คุณต้องเริ่มต้นด้วยแหล่งข้อมูลปัจจุบัน
snapshotและเรียกreloadItemsสิ่งนั้น คุณไม่สามารถสร้างสแนปชอตใหม่ได้คุณไม่สามารถพึ่งพาการ
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)
}
ฉันโพสต์คำถามเดียวกันไม่ได้ตระหนักถึง ฉันได้ผลโดยการแปลงโมเดลเป็นคลาสก่อน จากนั้นเรียก '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)
}
(ฉันได้ยื่นข้อบกพร่องเกี่ยวกับพฤติกรรมที่แสดงให้เห็นในคำถามนี้เพราะฉันไม่คิดว่ามันเป็นพฤติกรรมที่ดี แต่ในขณะที่สิ่งต่างๆยังคงอยู่ฉันคิดว่าฉันสามารถเดาได้ว่าความคิดนั้นมีจุดมุ่งหมายอย่างไร)
เมื่อคุณบอกสแน็ปช็อตไปยัง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)
}
}