Rewrite timeline top and bottom detection on top of CurrentValueSubjects

This commit is contained in:
Stefan Ceriu
2022-05-02 13:00:55 +03:00
parent 49bf8e2dcf
commit cd77a7e265
2 changed files with 50 additions and 54 deletions
@@ -22,23 +22,20 @@ class ListTableViewAdapter: NSObject, UITableViewDelegate {
private var contentOffsetObserverToken: NSKeyValueObservation? private var contentOffsetObserverToken: NSKeyValueObservation?
private var boundsObserverToken: NSKeyValueObservation? private var boundsObserverToken: NSKeyValueObservation?
private var isAtTop: Bool = false
private var isAtBottom: Bool = false
private var offsetDetails: ContentOffsetDetails? private var offsetDetails: ContentOffsetDetails?
private var draggingInitiated = false private var draggingInitiated = false
private var isAnimatingKeyboardAppearance = false private var isAnimatingKeyboardAppearance = false
private var previousFrame: CGRect = .zero private var previousFrame: CGRect = .zero
private(set) var tableView: UITableView? private(set) var tableView: UITableView?
let scrollViewDidRestPublisher = PassthroughSubject<Void, Never>() let scrollViewDidRestPublisher = PassthroughSubject<Void, Never>()
let scrollViewDidReachTopPublisher = PassthroughSubject<Void, Never>() let scrollViewTopVisiblePublisher = CurrentValueSubject<Bool, Never>(false)
let scrollViewBottomVisiblePublisher = PassthroughSubject<Bool, Never>() let scrollViewBottomVisiblePublisher = CurrentValueSubject<Bool, Never>(false)
override init() { override init() {
self.topDetectionOffset = 0.0 topDetectionOffset = 0.0
self.bottomDetectionOffset = 0.0 bottomDetectionOffset = 0.0
} }
init(tableView: UITableView, topDetectionOffset: CGFloat, bottomDetectionOffset: CGFloat) { init(tableView: UITableView, topDetectionOffset: CGFloat, bottomDetectionOffset: CGFloat) {
@@ -65,9 +62,9 @@ class ListTableViewAdapter: NSObject, UITableViewDelegate {
return return
} }
if isBottomVisible { if computeIsBottomVisible() {
offsetDetails = .bottomOffset offsetDetails = .bottomOffset
} else if isTopVisible { } else if computeIsTopVisible() {
if let topIndexPath = tableView.indexPathsForVisibleRows?.first { if let topIndexPath = tableView.indexPathsForVisibleRows?.first {
offsetDetails = .topOffset(previousVisibleIndexPath: topIndexPath, offsetDetails = .topOffset(previousVisibleIndexPath: topIndexPath,
previousItemCount: tableView.numberOfRows(inSection: 0)) previousItemCount: tableView.numberOfRows(inSection: 0))
@@ -101,27 +98,11 @@ class ListTableViewAdapter: NSObject, UITableViewDelegate {
} }
var isTracking: Bool { var isTracking: Bool {
self.tableView?.isTracking == true tableView?.isTracking == true
} }
var isDecelerating: Bool { var isDecelerating: Bool {
self.tableView?.isDecelerating == true tableView?.isDecelerating == true
}
var isTopVisible: Bool {
guard let scrollView = tableView else {
return false
}
return (scrollView.contentOffset.y + scrollView.adjustedContentInset.top) <= topDetectionOffset
}
var isBottomVisible: Bool {
guard let scrollView = tableView else {
return false
}
return (scrollView.contentOffset.y + self.bottomDetectionOffset) >= (scrollView.contentSize.height - scrollView.frame.size.height)
} }
func scrollToBottom(animated: Bool = false) { func scrollToBottom(animated: Bool = false) {
@@ -171,12 +152,12 @@ class ListTableViewAdapter: NSObject, UITableViewDelegate {
} }
private func handleScrollViewScroll() { private func handleScrollViewScroll() {
guard let tableView = self.tableView else { guard let tableView = tableView else {
return return
} }
let hasScrolledBecauseOfFrameChange = (previousFrame != tableView.frame) let hasScrolledBecauseOfFrameChange = (previousFrame != tableView.frame)
let shouldPinToBottom = isAtBottom && (isAnimatingKeyboardAppearance || hasScrolledBecauseOfFrameChange) let shouldPinToBottom = scrollViewBottomVisiblePublisher.value && (isAnimatingKeyboardAppearance || hasScrolledBecauseOfFrameChange)
if shouldPinToBottom { if shouldPinToBottom {
deregisterContentOffsetObserver() deregisterContentOffsetObserver()
@@ -187,35 +168,49 @@ class ListTableViewAdapter: NSObject, UITableViewDelegate {
return return
} }
let isTopVisible = self.isTopVisible let isTopVisible = computeIsTopVisible()
if isTopVisible && self.isAtTop != isTopVisible { if isTopVisible != scrollViewTopVisiblePublisher.value {
self.scrollViewDidReachTopPublisher.send(()) scrollViewTopVisiblePublisher.send(isTopVisible)
}
self.isAtTop = isTopVisible
let isBottomVisible = self.isBottomVisible
if self.isAtBottom != isBottomVisible {
self.scrollViewBottomVisiblePublisher.send(isBottomVisible)
self.isAtBottom = isBottomVisible
} }
if !self.draggingInitiated && tableView.isDragging { let isBottomVisible = computeIsBottomVisible()
self.draggingInitiated = true if isBottomVisible != scrollViewBottomVisiblePublisher.value {
} else if self.draggingInitiated && !tableView.isDragging { scrollViewBottomVisiblePublisher.send(isBottomVisible)
self.draggingInitiated = false }
self.scrollViewDidRestPublisher.send(())
if !draggingInitiated && tableView.isDragging {
draggingInitiated = true
} else if draggingInitiated && !tableView.isDragging {
draggingInitiated = false
scrollViewDidRestPublisher.send(())
} }
} }
@objc private func handlePanGesture(_ sender: UIPanGestureRecognizer) { @objc private func handlePanGesture(_ sender: UIPanGestureRecognizer) {
guard let tableView = self.tableView, guard let tableView = tableView,
sender.state == .ended, sender.state == .ended,
draggingInitiated == true, draggingInitiated == true,
!tableView.isDecelerating else { !tableView.isDecelerating else {
return return
} }
self.draggingInitiated = false draggingInitiated = false
self.scrollViewDidRestPublisher.send(()) scrollViewDidRestPublisher.send(())
}
private func computeIsTopVisible() -> Bool {
guard let scrollView = tableView else {
return false
}
return (scrollView.contentOffset.y + scrollView.adjustedContentInset.top) <= topDetectionOffset
}
private func computeIsBottomVisible() -> Bool {
guard let scrollView = tableView else {
return false
}
return (scrollView.contentOffset.y + bottomDetectionOffset) >= (scrollView.contentSize.height - scrollView.frame.size.height)
} }
} }
@@ -76,16 +76,16 @@ struct TimelineItemList: View {
.onReceive(scrollToBottomPublisher, perform: { .onReceive(scrollToBottomPublisher, perform: {
tableViewObserver.scrollToBottom(animated: true) tableViewObserver.scrollToBottom(animated: true)
}) })
.onReceive(tableViewObserver.scrollViewBottomVisiblePublisher, perform: { value in .onReceive(tableViewObserver.scrollViewTopVisiblePublisher, perform: { isTopVisible in
bottomVisiblePublisher.send(value) if !isTopVisible || context.viewState.isBackPaginating {
})
.onReceive(tableViewObserver.scrollViewDidReachTopPublisher, perform: {
if context.viewState.isBackPaginating {
return return
} }
attemptBackPagination() attemptBackPagination()
}) })
.onReceive(tableViewObserver.scrollViewBottomVisiblePublisher, perform: { isBottomVisible in
bottomVisiblePublisher.send(isBottomVisible)
})
.onChange(of: context.viewState.items) { _ in .onChange(of: context.viewState.items) { _ in
// Don't update the list while moving // Don't update the list while moving
if tableViewObserver.isDecelerating || tableViewObserver.isTracking { if tableViewObserver.isDecelerating || tableViewObserver.isTracking {
@@ -123,9 +123,10 @@ struct TimelineItemList: View {
return return
} }
if tableViewObserver.isTopVisible == false { if tableViewObserver.scrollViewTopVisiblePublisher.value == false {
return return
} }
context.send(viewAction: .loadPreviousPage) context.send(viewAction: .loadPreviousPage)
} }