Last active
February 1, 2024 00:42
-
-
Save brownsoo/5c1cb7fe1c6352ce080c5b4f4dc172d7 to your computer and use it in GitHub Desktop.
UIKit 콜렉션뷰 배치 업데이트 위한 비교 유틸
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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