随机“垂直跳跃”在 UICollectionViewController 中向上滚动时
我有一个 UIViewController,其中包含一个 UIPageViewController,它本身包含一个 UICollectionViewController。
UIPageViewController 上方:一个 UIView(名为 pagerView),它根据 UICollectionViewController 的垂直滚动偏移向上或向下移动。
最后,UIPageViewController 的顶部锚点被限制为 pagerView 的底部锚点。
问题是向上滚动不连续,UICollectionViewController有时会“跳跃”(可能是几百个点。)
为了重现:滚动到底部(第9行)然后开始向上滚动并看到它是如何跳转到第7行的。
UIViewController的源码:
import UIKit
protocol ViewControllerChildDelegate: AnyObject {
func childScrollViewWillBeginDragging(with offset: CGFloat)
func childScrollViewDidScroll(to offset: CGFloat)
}
class ViewController: UIViewController {
private lazy var pageViewController: UIPageViewController = {
let viewController = UIPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal, options: [:])
viewController.delegate = self
viewController.dataSource = self
return viewController
}()
private lazy var pagerView: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .brown
return view
}()
private let pagerViewHeight: CGFloat = 44
private var lastContentOffset: CGFloat = 0
lazy var pagerViewTopAnchorConstraint: NSLayoutConstraint = {
return pagerView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor)
}()
override func viewDidLoad() {
super.viewDidLoad()
title = "xxx"
view.addSubview(pagerView)
addChild(pageViewController)
view.addSubview(pageViewController.view)
pageViewController.didMove(toParent: self)
pageViewController.view.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
pagerViewTopAnchorConstraint,
pagerView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
pagerView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
pagerView.heightAnchor.constraint(equalToConstant: pagerViewHeight),
pageViewController.view.topAnchor.constraint(equalTo: pagerView.bottomAnchor),
pageViewController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
pageViewController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
pageViewController.view.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor)
])
let viewController = CustomViewController()
viewController.delegate = self
pageViewController.setViewControllers(
[viewController],
direction: .forward,
animated: false,
completion: nil
)
}
}
extension ViewController: UIPageViewControllerDataSource, UIPageViewControllerDelegate {
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
return nil
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
return nil
}
func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
}
}
extension ViewController: ViewControllerChildDelegate {
func childScrollViewWillBeginDragging(with offset: CGFloat) {
lastContentOffset = offset
}
func childScrollViewDidScroll(to offset: CGFloat) {
if lastContentOffset > offset {
view.layoutIfNeeded()
pagerViewTopAnchorConstraint.constant = 0
UIView.animate(withDuration: 0.3, animations: { [weak self] in
self?.view.layoutIfNeeded()
})
} else if lastContentOffset < offset {
view.layoutIfNeeded()
pagerViewTopAnchorConstraint.constant = -pagerViewHeight
UIView.animate(withDuration: 0.3, animations: { [weak self] in
self?.view.layoutIfNeeded()
})
}
}
}
整个代码可以在这个要点。还有一个XCode 项目可供下载。
另外:视频演示了该问题。
I have a UIViewController which includes a UIPageViewController which itself includes a UICollectionViewController.
Above the UIPageViewController: a UIView (named pagerView) which moves up or down depending on the vertical scrolling-offset of the UICollectionViewController.
Finally, the UIPageViewController's top anchor is constrained to pagerView's bottom anchor.
The problem is that scrolling up is not continuous, the UICollectionViewController sometimes "jumps" (it can be a few hundreds of points.)
In order to reproduce: scroll to the bottom (row 9) then start to scroll up and see how it jumps to row 7.
The source of the UIViewController:
import UIKit
protocol ViewControllerChildDelegate: AnyObject {
func childScrollViewWillBeginDragging(with offset: CGFloat)
func childScrollViewDidScroll(to offset: CGFloat)
}
class ViewController: UIViewController {
private lazy var pageViewController: UIPageViewController = {
let viewController = UIPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal, options: [:])
viewController.delegate = self
viewController.dataSource = self
return viewController
}()
private lazy var pagerView: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .brown
return view
}()
private let pagerViewHeight: CGFloat = 44
private var lastContentOffset: CGFloat = 0
lazy var pagerViewTopAnchorConstraint: NSLayoutConstraint = {
return pagerView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor)
}()
override func viewDidLoad() {
super.viewDidLoad()
title = "xxx"
view.addSubview(pagerView)
addChild(pageViewController)
view.addSubview(pageViewController.view)
pageViewController.didMove(toParent: self)
pageViewController.view.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
pagerViewTopAnchorConstraint,
pagerView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
pagerView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
pagerView.heightAnchor.constraint(equalToConstant: pagerViewHeight),
pageViewController.view.topAnchor.constraint(equalTo: pagerView.bottomAnchor),
pageViewController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
pageViewController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
pageViewController.view.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor)
])
let viewController = CustomViewController()
viewController.delegate = self
pageViewController.setViewControllers(
[viewController],
direction: .forward,
animated: false,
completion: nil
)
}
}
extension ViewController: UIPageViewControllerDataSource, UIPageViewControllerDelegate {
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
return nil
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
return nil
}
func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
}
}
extension ViewController: ViewControllerChildDelegate {
func childScrollViewWillBeginDragging(with offset: CGFloat) {
lastContentOffset = offset
}
func childScrollViewDidScroll(to offset: CGFloat) {
if lastContentOffset > offset {
view.layoutIfNeeded()
pagerViewTopAnchorConstraint.constant = 0
UIView.animate(withDuration: 0.3, animations: { [weak self] in
self?.view.layoutIfNeeded()
})
} else if lastContentOffset < offset {
view.layoutIfNeeded()
pagerViewTopAnchorConstraint.constant = -pagerViewHeight
UIView.animate(withDuration: 0.3, animations: { [weak self] in
self?.view.layoutIfNeeded()
})
}
}
}
The whole code can be viewed in this gist. There is also a XCode project to download.
And also: a video demonstrating the problem.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(2)
在花了一些时间研究你的项目后,我还没有找到解决方案,但我相信我已经找到了可以引导你解决问题的东西。
以下是我的观察结果:
当您从
CustomViewController
向上/向下滚动collectionView
时,您将使用通过ViewControllerChildDelegate
更新的滚动偏移量来调整pagerViewTopAnchorConstraint
。调整此约束还会调整
pageViewController
的高度,进而调整CustomViewController
的collectionView
高度调整
collectionView
的高度可能会导致其contentOffset
发生跳跃,具体取决于其当前偏移量。这是解释这一点的答案:https://stackoverflow.com/a/21787754/9293498为了验证这一点,您可以评论更新
pagerViewTopAnchorConstraint
的部分,您会发现这样做将消除“跳跃”为了进一步验证情况是否确实如此,我创建了两个 KVO 观察者:一个用于 collectionView 的
contentSize
,另一个用于 collectionView 的contentOffset
,如下所示:我注意到我可以根据“找到你!”中的值使用断点捕获“跳转”。检查。基本上,当您到达第 9 行并尝试向上滚动时,每当“跳转”即将发生时都会发生两件事:
collectionView
的contentOffset
会根据我的collectionView.contentSize.height - collectionView.frame.height
中的值进行更新我猜测是由于框架的变化collectionView
的contentSize
高度也大幅下降/上升(我说的是 1000 秒),这与您的相差甚远寻呼机
的高度(44)这几个变化加在一起就是我们注意到的“跳转”,它在帧更改期间的一些递归调用后自行解决。
After spending some time with your project, I haven't found a fix yet but I believe I've found things which can point you towards a fix.
Here are my observations:
As you scroll your
collectionView
fromCustomViewController
up/down, you are using the updated scroll offset throughViewControllerChildDelegate
to adjustpagerViewTopAnchorConstraint
.Adjusting this constraint also adjusts the height of your
pageViewController
which in turn adjusts yourCustomViewController
'scollectionView
heightAdjusting the
collectionView
's height can cause jumps in itscontentOffset
depending on its current offset. Here's an answer which explains this: https://stackoverflow.com/a/21787754/9293498Just to fact-check this, you can comment the parts where you update your
pagerViewTopAnchorConstraint
, and you'll notice that doing so will get rid of the 'jumps'To further verify if this was indeed the case, I created two KVO observers: One for the collectionView's
contentSize
and the other for collectionView'scontentOffset
as such:where I noticed I could catch the 'jumps' with breakpoints based on the values in my "Found you!" checks. Basically, when you reach Row 9 and try to scroll up, there are two things happening whenever a 'jump' is about to happen:
collectionView
'scontentOffset
gets updated based on the value fromcollectionView.contentSize.height - collectionView.frame.height
which I'm guessing is attributed to the frame changecollectionView
'scontentSize
also gets a massive drop/rise in height (I'm talking 1000s) which is far from yourpager
's height (which is 44)These couple changes added together is what we notice as a 'jump' which sorts itself out after some recursive calls during frame change.
查看提供的视频,问题似乎是由于可重复使用单元的实施造成的。
当您第一次启动应用程序时,将有 n 个单元(在您的例子中为 4 个)被创建为 n 个实例。此时,n个实例都在内存中。当您向下滚动时,您不会看到任何问题,导致屏幕上的单元格数量减少,但您仍然没有当场创建任何单元格。另一方面,当向上滚动时,您有 n=2 个单元格,并且在滚动时突然需要更多单元格,这会在该位置产生丢失的单元格并导致滞后跳跃。
在典型的表格视图中,可见单元格的数量是恒定的,因此您将始终在内存中缓存相同的数量,并在它超出屏幕时重用它。
总结一下:
从 n 到 n ->没问题,您可能缓存 n+1 个,因此您始终有单元格可以重用
从 n 到 k (k < n) ->没问题,滚动时您将减少缓存的单元格
从 n 到 k (k > n) ->导致滞后,您必须当场创建可重用的单元格,或者重用屏幕上已经显示的任何内容。
您可以通过简单地将
collectionView.dequeueReusableCell
替换为您自己存储的数组来解决此问题启动细胞并重复使用它们。这当然需要特殊处理,但我相信它会解决您的问题希望这个解释有帮助:)
Looking at the video provided, the issue seems to be due to the implementation of reusable cells.
When you first launch the app, there would be n cells (4 in your case) which are created as n instances. At this point, the n instances are in memory. when you scroll down, you don't see any issue cause the number of cells on screen reduces but still you are not creating any cells on the spot. On the other hand, when scrolling upward, you have n=2 cells and you suddenly need more when scrolling which creates the missing cells at the spot and causes the lagging jumps.
In a typical tableview, the number of visible cells is constant, such that you will always cache the same number in memory and reuse it when it goes beyond the screen.
To sum it up:
going from n to n -> no issue, you cache probably n+1 so you always have cells to reuse
going from n to k (k < n) -> no issue, you'll reduce the cached cell as you scroll
going from n to k (k > n) -> causes lagging, you'll have to create the reusable cells on the spot or reuse whatever already shown on screen while you can
You can fix this by simply replacing
collectionView.dequeueReusableCell
with you own array where you store the initiated cells and reuse them. This of course will require special handling but I believe it would fix your issueHope this explanation helps :)