Reputation: 55
How to Detect Scroll Completion Before Calculating Frame in Swift (iOS)?
I am working on an iOS application where I have a screen containing both a UICollectionView and a UITableView. The UICollectionView acts as a tab selector, and when an item is selected, the UITableView updates its content accordingly.
I am implementing an onboarding/tutorial feature using EasyTipView, where I highlight specific cells in both the UICollectionView and UITableView. The flow works as follows: 1. Show the first tip. 2. Scroll the UICollectionView to select a tab. 3. Once the tab is selected, update the UITableView and scroll to the corresponding cell if needed. 4. Find the frame of the target cell and pass it to comletion.
Problem
The frame calculation is happening too early, causing incorrect placements of tips. I am currently using DispatchQueue.main.asyncAfter with a delay to wait for the scrolling to finish, but this approach is unreliable because: • Different devices have different scrolling speeds. • Scrolling to distant cells takes longer.
Expected Behavior
I want to detect when scrolling is fully completed, ensuring that the frame of the target cell is accurate before passing it to EasyTipView.
Current Attempt
I am using scrollViewDidEndScrollingAnimation and scrollViewDidEndDecelerating to detect when scrolling stops:
func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
handleScrollCompletion(for: scrollView)
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
handleScrollCompletion(for: scrollView)
}
private func handleScrollCompletion(for scrollView: UIScrollView, completion: (() -> Void)? = nil) {
if scrollView == tableView, isTableViewScrolling {
isTableViewScrolling = false
completion?()
} else if scrollView == collectionView, isCollectionViewScrolling {
isCollectionViewScrolling = false
completion?()
}
}
private func checkIfScrollFinished(for scrollView: UIScrollView, completion: (() -> Void)? = nil) {
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
if !scrollView.isDragging && !scrollView.isDecelerating {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
self.handleScrollCompletion(for: scrollView, completion: completion)
}
}
}
}
CollectionView Logic:
private func scrollToTabAndGetFrame(for selectedTab: tabData, completion: ((CGRect?) -> Void)? = nil) {
isCollectionViewScrolling = true
viewModel?.selectedTab = selectedTab // -> Scrolls automatically when selectedTab changes
DispatchQueue.main.async {
self.collectionView.layoutIfNeeded()
self.checkIfScrollFinished(for: self.collectionView) { [weak self] in
guard let self = self else { return }
guard let index = self.viewModel?.tabData.firstIndex(where: { $0 == selectedTab }) else { return }
let indexPath = IndexPath(row: index, section: 0)
if let cell = self.collectionView.cellForItem(at: indexPath) {
let cellFrame = self.collectionView.convert(cell.frame, to: self.view)
completion?(cellFrame)
} else {
completion?(nil)
}
}
}
}
TableView:
func scrollToCellAndGetFrame(for selectedTab: tabData, for cell: cellData, completion: ((CGRect?) -> Void)? = nil) {
isTableViewScrolling = true
viewModel?.selectedTab = selectedTab
DispatchQueue.main.async {
self.collectionView.layoutIfNeeded()
self.checkIfScrollFinished(for: self.collectionView) { [weak self] in
guard let self = self else { return }
guard let index = self.viewModel?.tabSections.firstIndex(where: { $0 == sectionType }) else { return }
let indexPath = IndexPath(row: index, section: 0)
self.tableView.scrollToRow(at: indexPath, at: .middle, animated: true)
self.checkIfScrollFinished(for: self.tableView) { [weak self] in
guard let self = self else { return }
if let cell = self.tableView.cellForRow(at: indexPath) {
let cellFrame = self.tableView.convert(cell.frame, to: self.view)
completion?(cellFrame)
} else {
completion?(nil)
}
}
}
}
}
How can I reliably detect when both UICollectionView and UITableView scrolling is fully completed, ensuring the cell is properly visible before calculating its frame?
Is there a better approach than using scrollViewDidEndScrollingAnimation and scrollViewDidEndDecelerating?
Any suggestions for improving this implementation would be greatly appreciated!
Upvotes: 0
Views: 31