optimized item identifiers to use merged item directly

This commit is contained in:
Avently committed 2025-01-16 04:55:24 -08:00
1 parent 890e35b902
commit 31ee89c3c7
4 files changed
+37 -99

No files matched your search

+8 -3
View File
@@ -952,12 +952,17 @@ final class ChatModel: ObservableObject {
// returns the previous member in the same merge group and the count of members in this group // returns the previous member in the same merge group and the count of members in this group
func getPrevHiddenMember(_ member: GroupMember, _ range: ClosedRange<Int>) -> (GroupMember?, Int) { func getPrevHiddenMember(_ member: GroupMember, _ range: ClosedRange<Int>) -> (GroupMember?, Int) {
let items = im.reversedChatItems
var prevMember: GroupMember? = nil var prevMember: GroupMember? = nil
var memberIds: Set<Int64> = [] var memberIds: Set<Int64> = []
for i in range { for i in range {
if case let .groupRcv(m) = im.reversedChatItems[i].chatDir { if i < items.count {
if prevMember == nil && m.groupMemberId != member.groupMemberId { prevMember = m } if case let .groupRcv(m) = items[i].chatDir {
memberIds.insert(m.groupMemberId) if prevMember == nil && m.groupMemberId != member.groupMemberId { prevMember = m }
memberIds.insert(m.groupMemberId)
}
} else {
logger.error("getPrevHiddenMember: index >= count of reversed items: \(i) vs \(items.count)")
} }
} }
return (prevMember, memberIds.count) return (prevMember, memberIds.count)
@@ -114,15 +114,11 @@ struct MergedItems {
} }
enum MergedItem: /*Identifiable, */Hashable { enum MergedItem: Hashable, Equatable {
// var id: Int64 { // equatable and hashable implementations allows to NSDiffableDataSourceSnapshot to see the difference and correcrly scroll items we want. Without any of it, the scroll position will be random in ReverseList
// get { static func == (lhs: Self, rhs: Self) -> Bool {
// switch self { lhs.hashValue == rhs.hashValue
// case let .single(item, _): item.item.id }
// case let .grouped(items, _, _, _, _, _, _): items.boxedValue.last!.item.id
// }
// }
// }
var hashValue: Int { self.newest().item.hashValue } var hashValue: Int { self.newest().item.hashValue }
@@ -275,26 +271,7 @@ class ActiveChatState {
} }
} }
struct BoxedValue2<T: Hashable>: /*Identifiable, */Hashable { class BoxedValue<T: Hashable>: Equatable, Hashable {
// var id: Int64 { (boxedValue as! MergedItem).id }
static func == (lhs: BoxedValue2<T>, rhs: BoxedValue2<T>) -> Bool {
lhs.boxedValue == rhs.boxedValue
}
var hashValue: Int { (boxedValue as! MergedItem).newest().hashValue }
func hash(into hasher: inout Hasher) {
hasher.combine("\((boxedValue as! MergedItem).newest())")
}
var boxedValue : T
init(_ value: T) {
self.boxedValue = value
}
}
class BoxedValue<T: Hashable>: Hashable {
static func == (lhs: BoxedValue<T>, rhs: BoxedValue<T>) -> Bool { static func == (lhs: BoxedValue<T>, rhs: BoxedValue<T>) -> Bool {
lhs.boxedValue == rhs.boxedValue lhs.boxedValue == rhs.boxedValue
} }
@@ -313,7 +290,7 @@ extension ReverseList.Controller {
@MainActor @MainActor
func visibleItemIndexesNonReversed(_ mergedItems: Binding<MergedItems>) -> ClosedRange<Int> { func visibleItemIndexesNonReversed(_ mergedItems: Binding<MergedItems>) -> ClosedRange<Int> {
let zero = 0 ... 0 let zero = 0 ... 0
if itemCount == 0 { if mergedItems.wrappedValue.items.count == 0 {
return zero return zero
} }
let listState = getListState() ?? ListState() let listState = getListState() ?? ListState()
+1 -1
View File
@@ -445,7 +445,7 @@ struct ChatView: View {
return GeometryReader { g in return GeometryReader { g in
let _ = logger.debug("LALAL RELOAD \(im.reversedChatItems.count)") let _ = logger.debug("LALAL RELOAD \(im.reversedChatItems.count)")
// LALAL CAN I CHANGE BINDING LIKE THIS IN ignoreLoadingRequests? // LALAL CAN I CHANGE BINDING LIKE THIS IN ignoreLoadingRequests?
ReverseList(items: im.reversedChatItems, mergedItems: $mergedItems, revealedItems: $revealedItems, unreadCount: Binding.constant(chat.chatStats.unreadCount), scrollState: $scrollModel.state, loadingMoreItems: $loadingMoreItems, allowLoadMoreItems: $allowLoadMoreItems, ignoreLoadingRequests: searchValueIsEmpty ? $ignoreLoadingRequests : Binding.constant([])) { index, mergedItem in ReverseList(mergedItems: mergedItems, revealedItems: $revealedItems, unreadCount: Binding.constant(chat.chatStats.unreadCount), scrollState: $scrollModel.state, loadingMoreItems: $loadingMoreItems, allowLoadMoreItems: $allowLoadMoreItems, ignoreLoadingRequests: searchValueIsEmpty ? $ignoreLoadingRequests : Binding.constant([])) { index, mergedItem in
let ci = switch mergedItem { let ci = switch mergedItem {
case let .single(item, _): item.item case let .single(item, _): item.item
case let .grouped(items, _, _, _, _, _, _): items.boxedValue.last!.item case let .grouped(items, _, _, _, _, _, _): items.boxedValue.last!.item
+21 -65
View File
@@ -12,8 +12,7 @@ import SimpleXChat
/// A List, which displays it's items in reverse order - from bottom to top /// A List, which displays it's items in reverse order - from bottom to top
struct ReverseList<Content: View>: UIViewControllerRepresentable { struct ReverseList<Content: View>: UIViewControllerRepresentable {
let items: Array<ChatItem> var mergedItems: MergedItems
@Binding var mergedItems: MergedItems
@Binding var revealedItems: Set<Int64> @Binding var revealedItems: Set<Int64>
@Binding var unreadCount: Int @Binding var unreadCount: Int
@@ -37,11 +36,11 @@ struct ReverseList<Content: View>: UIViewControllerRepresentable {
func updateUIViewController(_ controller: Controller, context: Context) { func updateUIViewController(_ controller: Controller, context: Context) {
controller.representer = self controller.representer = self
if case let .scrollingTo(destination) = scrollState, !items.isEmpty, !controller.scrollToItemInProgress { if case let .scrollingTo(destination) = scrollState, !mergedItems.items.isEmpty, !controller.scrollToItemInProgress {
controller.view.layer.removeAllAnimations() controller.view.layer.removeAllAnimations()
switch destination { switch destination {
case let .item(id): case let .item(id):
let row = items.firstIndex(where: { $0.id == id }) let row = mergedItems.indexInParentItems[id]
if let row { if let row {
logger.debug("LALAL SCROLLING TO \(row)") logger.debug("LALAL SCROLLING TO \(row)")
controller.scroll(to: row, position: .bottom) controller.scroll(to: row, position: .bottom)
@@ -64,11 +63,11 @@ struct ReverseList<Content: View>: UIViewControllerRepresentable {
// so it's better to just wait until dragging ends // so it's better to just wait until dragging ends
if waitOnEndedScrolling && controller.tableView.isDragging/* && !controller.tableView.isDecelerating*/ { if waitOnEndedScrolling && controller.tableView.isDragging/* && !controller.tableView.isDecelerating*/ {
controller.runBlockOnEndDecelerating = { controller.runBlockOnEndDecelerating = {
controller.update(items: items) controller.update()
} }
} else { } else {
controller.runBlockOnEndDecelerating = nil controller.runBlockOnEndDecelerating = nil
controller.update(items: items) controller.update()
} }
} }
} }
@@ -77,17 +76,7 @@ struct ReverseList<Content: View>: UIViewControllerRepresentable {
public class Controller: UITableViewController { public class Controller: UITableViewController {
private enum Section { case main } private enum Section { case main }
var representer: ReverseList var representer: ReverseList
// Here Int means hash of the ChatItem that is inside MergedItem.newest().item.hashValue. private var dataSource: UITableViewDiffableDataSource<Section, MergedItem>!
// Putting MergedItem here directly prevents UITableViewDiffableDataSource to make partial updates
// which looks like UITableView scrolls to bottom on insert values to bottom instead of
// remains in the same scroll position
private var dataSource: UITableViewDiffableDataSource<Section, Int>!
var itemCount: Int {
get {
representer.mergedItems.items.count
}
}
private var itemsInPrevSnapshot: Dictionary<Int, MergedItem> = [:]
private let updateFloatingButtons = PassthroughSubject<Void, Never>() private let updateFloatingButtons = PassthroughSubject<Void, Never>()
private var bag = Set<AnyCancellable>() private var bag = Set<AnyCancellable>()
@@ -128,17 +117,17 @@ struct ReverseList<Content: View>: UIViewControllerRepresentable {
} }
// 3. Configure data source // 3. Configure data source
self.dataSource = UITableViewDiffableDataSource<Section, Int>( self.dataSource = UITableViewDiffableDataSource<Section, MergedItem>(
tableView: tableView tableView: tableView
) { (tableView, indexPath, item) -> UITableViewCell? in ) { (tableView, indexPath, item) -> UITableViewCell? in
let cell = tableView.dequeueReusableCell(withIdentifier: cellReuseId, for: indexPath) let cell = tableView.dequeueReusableCell(withIdentifier: cellReuseId, for: indexPath)
if #available(iOS 16.0, *) { if #available(iOS 16.0, *) {
cell.contentConfiguration = UIHostingConfiguration { self.representer.content(indexPath.item, self.itemsInPrevSnapshot[item]!) } cell.contentConfiguration = UIHostingConfiguration { self.representer.content(indexPath.item, item) }
.margins(.all, 0) .margins(.all, 0)
.minSize(height: 1) // Passing zero will result in system default of 44 points being used .minSize(height: 1) // Passing zero will result in system default of 44 points being used
} else { } else {
if let cell = cell as? HostingCell<Content> { if let cell = cell as? HostingCell<Content> {
cell.set(content: self.representer.content(indexPath.item, self.itemsInPrevSnapshot[item]!), parent: self) cell.set(content: self.representer.content(indexPath.item, item), parent: self)
} else { } else {
fatalError("Unexpected Cell Type for: \(item)") fatalError("Unexpected Cell Type for: \(item)")
} }
@@ -269,46 +258,26 @@ struct ReverseList<Content: View>: UIViewControllerRepresentable {
} }
} }
func update(items: [ChatItem]) { func update() {
var prevSnapshot = dataSource.snapshot() let prevSnapshot = dataSource.snapshot()
let wasCount = prevSnapshot.numberOfItems let wasCount = prevSnapshot.numberOfItems
let willBeCount = representer.mergedItems.items.count let items = representer.mergedItems.items
let willBeCount = items.count
let c = 1 let c = 1
let insertedSeveralNewestItems = wasCount != 0 && willBeCount - wasCount == c && prevSnapshot.itemIdentifiers.first!.hashValue == self.representer.mergedItems.items[c].hashValue let insertedSeveralNewestItems = wasCount != 0 && willBeCount - wasCount == c && prevSnapshot.itemIdentifiers.first!.hashValue == self.representer.mergedItems.items[c].hashValue
logger.debug("LALAL WAS \(wasCount) will be \(self.representer.mergedItems.items.count)") logger.debug("LALAL WAS \(wasCount) will be \(self.representer.mergedItems.items.count)")
//self.representer.mergedItems = MergedItems.create(items, representer.unreadCount, representer.revealedItems, ItemsModel.shared.chatState) if items == prevSnapshot.itemIdentifiers {
let snapshot: NSDiffableDataSourceSnapshot<Section, Int>
let itemsInCurrentSnapshot: Dictionary<Int, MergedItem>
if insertedSeveralNewestItems {
var new = itemsInPrevSnapshot
for i in 0 ..< c {
prevSnapshot.insertItems([representer.mergedItems.items[c - i].hashValue], beforeItem: prevSnapshot.itemIdentifiers.first!)
new[representer.mergedItems.items[c - i].hashValue] = representer.mergedItems.items[c - i]
}
itemsInCurrentSnapshot = new
snapshot = prevSnapshot
} else {
logger.debug("LALAL BEFORE snap")
var new: Dictionary<Int, MergedItem> = [:]
let identifiers = representer.mergedItems.items.map({ merged in
new[merged.hashValue] = merged
return merged.hashValue
})
logger.debug("LALAL AFTER snap")
if identifiers == prevSnapshot.itemIdentifiers {
logger.debug("LALAL SAME ITEMS, not rebuilding the tableview") logger.debug("LALAL SAME ITEMS, not rebuilding the tableview")
// update counters because they are static, unbound to specific chat and will become outdated if a new empty chat was open after non-empty one with unread messages // update counters because they are static, unbound to specific chat and will become outdated if a new empty chat was open after non-empty one with unread messages
updateFloatingButtons.send() updateFloatingButtons.send()
return return
} }
var snap = NSDiffableDataSourceSnapshot<Section, Int>() var snapshot = NSDiffableDataSourceSnapshot<Section, MergedItem>()
snap.appendSections([.main]) snapshot.appendSections([.main])
snap.appendItems(identifiers) snapshot.appendItems(items)
itemsInCurrentSnapshot = new
snapshot = snap
}
dataSource.defaultRowAnimation = .none dataSource.defaultRowAnimation = .none
let wasContentHeight = tableView.contentSize.height let wasContentHeight = tableView.contentSize.height
let wasOffset = tableView.contentOffset.y let wasOffset = tableView.contentOffset.y
let listState = getListState() let listState = getListState()
@@ -317,12 +286,11 @@ struct ReverseList<Content: View>: UIViewControllerRepresentable {
let countDiff = max(0, snapshot.numberOfItems - prevSnapshot.numberOfItems) let countDiff = max(0, snapshot.numberOfItems - prevSnapshot.numberOfItems)
// Sets content offset on initial load // Sets content offset on initial load
if wasCount == 0 { if wasCount == 0 {
itemsInPrevSnapshot = itemsInCurrentSnapshot
dataSource.apply( dataSource.apply(
snapshot, snapshot,
animatingDifferences: insertedSeveralNewestItems animatingDifferences: insertedSeveralNewestItems
) )
if let firstUnreadItem = snapshot.itemIdentifiers.lastIndex(where: { hash in itemsInPrevSnapshot[hash]!.hasUnread() }) { if let firstUnreadItem = snapshot.itemIdentifiers.lastIndex(where: { $0.hasUnread() }) {
scrollToRowWhenKnowSize(firstUnreadItem) scrollToRowWhenKnowSize(firstUnreadItem)
} else { } else {
tableView.setContentOffset( tableView.setContentOffset(
@@ -332,18 +300,14 @@ struct ReverseList<Content: View>: UIViewControllerRepresentable {
} }
} else if wasCount != snapshot.numberOfItems { } else if wasCount != snapshot.numberOfItems {
logger.debug("LALAL drag \(self.tableView.isDragging), decel \(self.tableView.isDecelerating)") logger.debug("LALAL drag \(self.tableView.isDragging), decel \(self.tableView.isDecelerating)")
// tableView.panGestureRecognizer.isEnabled = false
// logger.debug("LALAL drag2 \(self.tableView.isDragging), decel \(self.tableView.isDecelerating)")
if useSmoothScrolling && tableView.isDecelerating { if useSmoothScrolling && tableView.isDecelerating {
// CATransaction.begin() tableView.panGestureRecognizer.isEnabled = false
itemsInPrevSnapshot = itemsInCurrentSnapshot
tableView.beginUpdates() tableView.beginUpdates()
dataSource.apply( dataSource.apply(
snapshot, snapshot,
animatingDifferences: false animatingDifferences: false
) )
tableView.endUpdates() tableView.endUpdates()
// CATransaction.commit()
tableView.panGestureRecognizer.isEnabled = true tableView.panGestureRecognizer.isEnabled = true
} else { } else {
// remember current translation // remember current translation
@@ -354,7 +318,6 @@ struct ReverseList<Content: View>: UIViewControllerRepresentable {
translationToApply = t translationToApply = t
} }
} }
itemsInPrevSnapshot = itemsInCurrentSnapshot
dataSource.apply( dataSource.apply(
snapshot, snapshot,
animatingDifferences: false animatingDifferences: false
@@ -516,13 +479,6 @@ struct ReverseList<Content: View>: UIViewControllerRepresentable {
await preloadItems(self.representer.mergedItems, self.representer.allowLoadMoreItems, state, self.representer.$ignoreLoadingRequests) { pagination in await preloadItems(self.representer.mergedItems, self.representer.allowLoadMoreItems, state, self.representer.$ignoreLoadingRequests) { pagination in
logger.debug("LALAL LOADING INSIDE") logger.debug("LALAL LOADING INSIDE")
let triedToLoad = await self.representer.loadItems(false, pagination, { self.visibleItemIndexesNonReversed(Binding.constant(self.representer.mergedItems)) }) let triedToLoad = await self.representer.loadItems(false, pagination, { self.visibleItemIndexesNonReversed(Binding.constant(self.representer.mergedItems)) })
let superview = self.tableView.superview
if triedToLoad, let superview {
// let t = self.tableView.panGestureRecognizer.translation(in: superview)
// if t.y != 0 {
// self.translationToApply = t
// }
}
return triedToLoad return triedToLoad
} }
} }