Skip to content

Instantly share code, notes, and snippets.

@brownsoo
Last active February 1, 2024 00:42
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save brownsoo/5c1cb7fe1c6352ce080c5b4f4dc172d7 to your computer and use it in GitHub Desktop.
Save brownsoo/5c1cb7fe1c6352ce080c5b4f4dc172d7 to your computer and use it in GitHub Desktop.
UIKit 콜렉션뷰 배치 업데이트 위한 비교 유틸
import Foundation
import UIKit
typealias BatchUpdatable = Identifiable & Equatable
/// 배치 업데이트를 위한 비교 모델
///
struct BatchUpdates {
let deleted: [Int]
let inserted: [Int]
let moved: [(Int, Int)]
let reloaded: [Int]
var hasChanges: Bool {
return deleted.count > 0 || inserted.count > 0 || moved.count > 0 || reloaded.count > 0
}
var hasOnlyDeletion: Bool {
return deleted.count > 0 && inserted.count == 0 && moved.count == 0 && reloaded.count == 0
}
var hasOnlyInsertion: Bool {
return deleted.count == 0 && inserted.count > 0 && moved.count == 0 && reloaded.count == 0
}
static func compare<T: BatchUpdatable>(oldValues: [T], newValues: [T]) -> BatchUpdates {
var deleted = [Int]()
var moved = [(Int, Int)]()
var reloaded = [Int]()
var remainingNewValues = newValues
.enumerated()
.map {
(element: $0.element, offset: $0.offset, alreadyFound: false)
}
outer: for oldValue in oldValues.enumerated() {
for newValue in remainingNewValues {
if oldValue.element.id == newValue.element.id && !newValue.alreadyFound {
// 같은 아이덴티티 아이템
if oldValue.offset == newValue.offset {
if oldValue.element != newValue.element {
reloaded.append(newValue.offset)
}
} else if !deleted.contains(newValue.offset) {
moved.append((oldValue.offset, newValue.offset))
}
remainingNewValues[newValue.offset].alreadyFound = true
continue outer
}
}
deleted.append(oldValue.offset)
}
let inserted = remainingNewValues
.filter { !$0.alreadyFound }
.map { $0.offset }
return BatchUpdates(deleted: deleted, inserted: inserted, moved: moved, reloaded: reloaded)
}
}
extension UITableView {
func reloadData(with batchUpdates: BatchUpdates, in section: Int = 0, completion: ((Bool) -> Void)? = nil) {
debugPrint(batchUpdates)
if !batchUpdates.hasChanges {
completion?(true)
return
}
debugPrint("performBatchUpdates-->")
performBatchUpdates ({
deleteRows(at: batchUpdates.deleted.map { IndexPath(row: $0, section: section) }, with: .left)
insertRows(at: batchUpdates.inserted.map { IndexPath(row: $0, section: section) }, with: .right)
reloadRows(at: batchUpdates.reloaded.map { IndexPath(row: $0, section: section) }, with: .none)
// FIXME: 삭제, 추가와 이동이 같이 되는지..
if batchUpdates.deleted.isEmpty && batchUpdates.inserted.isEmpty {
for movedRows in batchUpdates.moved {
self.moveRow(
at: IndexPath(row: movedRows.0, section: section),
to: IndexPath(row: movedRows.1, section: section))
}
}
}, completion: completion)
}
/// Section 포함 전부 업데이트, (섹터간 이동 제외 ㅜㅜ)
func reloadData(sectionUpdates: BatchUpdates,
batchUpdatesWithSection: [(Int, BatchUpdates)],
withAnimation animating: Bool = true,
completion: ((Bool) -> Void)? = nil) {
debugPrint("batchUpdatesWithSection-->")
performBatchUpdates ({
deleteSections(IndexSet(sectionUpdates.deleted), with: animating ? .left : .none)
insertSections(IndexSet(sectionUpdates.inserted), with: animating ? .right : .none)
reloadSections(IndexSet(sectionUpdates.reloaded), with: .none)
if sectionUpdates.deleted.isEmpty && sectionUpdates.inserted.isEmpty {
for movedSections in sectionUpdates.moved {
moveSection(movedSections.0, toSection: movedSections.1)
}
}
for sectionUpdate in batchUpdatesWithSection {
let section = sectionUpdate.0
let batchUpdates = sectionUpdate.1
deleteRows(at: batchUpdates.deleted.map { IndexPath(row: $0, section: section) }, with: animating ? .left : .none)
insertRows(at: batchUpdates.inserted.map { IndexPath(row: $0, section: section) }, with: animating ? .right : .none)
reloadRows(at: batchUpdates.reloaded.map { IndexPath(row: $0, section: section) }, with: .none)
// FIXME: 삭제, 추가와 이동이 같이 되는지..
if batchUpdates.deleted.isEmpty && batchUpdates.inserted.isEmpty {
for movedRows in batchUpdates.moved {
moveRow(at: IndexPath(row: movedRows.0, section: section),
to: IndexPath(row: movedRows.1, section: section))
}
}
}
}, completion: completion)
}
}
extension UICollectionView {
func reloadData(with batchUpdates: BatchUpdates,
in section: Int = 0,
completion: ((Bool) -> Void)? = nil) {
debugPrint(batchUpdates)
if !batchUpdates.hasChanges {
completion?(true)
return
}
debugPrint("performBatchUpdates-->")
performBatchUpdates({
deleteItems(at: batchUpdates.deleted.map { IndexPath(row: $0, section: section) })
insertItems(at: batchUpdates.inserted.map { IndexPath(row: $0, section: section) })
reloadItems(at: batchUpdates.reloaded.map { IndexPath(row: $0, section: section) })
if batchUpdates.deleted.isEmpty && batchUpdates.inserted.isEmpty {
for movedRows in batchUpdates.moved {
moveItem(at: IndexPath(row: movedRows.0, section: section),
to: IndexPath(row: movedRows.1, section: section))
}
}
}, completion: { [weak self] success in
guard let self = self else { return }
// 삭제와 추가, 이동이 같이 안되네..
if batchUpdates.deleted.isEmpty && batchUpdates.inserted.isEmpty {
self.performBatchUpdates({
for movedRows in batchUpdates.moved {
self.moveItem(at: IndexPath(row: movedRows.0, section: section),
to: IndexPath(row: movedRows.1, section: section))
}
}, completion: completion)
} else {
completion?(success)
}
})
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment