随机“垂直跳跃”在 UICollectionViewController 中向上滚动时

发布于 2025-01-15 14:59:56 字数 4683 浏览 1 评论 0原文

我有一个 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 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

阪姬 2025-01-22 14:59:56

在花了一些时间研究你的项目后,我还没有找到解决方案,但我相信我已经找到了可以引导你解决问题的东西。

以下是我的观察结果:

  1. 当您从 CustomViewController 向上/向下滚动 collectionView 时,您将使用通过 ViewControllerChildDelegate 更新的滚动偏移量来调整pagerViewTopAnchorConstraint

  2. 调整此约束还会调整 pageViewController 的高度,进而调整 CustomViewControllercollectionView 高度

  3. 调整 collectionView 的高度可能会导致其 contentOffset 发生跳跃,具体取决于其当前偏移量。这是解释这一点的答案:https://stackoverflow.com/a/21787754/9293498

    为了验证这一点,您可以评论更新 pagerViewTopAnchorConstraint 的部分,您会发现这样做将消除“跳跃”

  4. 为了进一步验证情况是否确实如此,我创建了两个 KVO 观察者:一个用于 collectionView 的 contentSize ,另一个用于 collectionView 的 contentOffset ,如下所示:

     contentSizeObserver = collectionView.observe(\.contentSize) { [weak self] collectionView, _ in
         //忽略“找到你!”的值检查,因为它相对于我的设备
         如果collectionView.contentSize.height < 3000 {
             print("找到你了!")
         }
         print("内容大小设置:", collectionView.contentSize)
     }
    
     contentOffsetObserver = collectionView.observe(\.contentOffset) { [weak self] collectionView, _ in
         //忽略“找到你!”的值检查,因为它相对于我的设备
         如果 collectionView.contentOffset.y < 1460 {
             print("找到你了!")
         }
         print("内容偏移设置:", collectionView.contentOffset)
     }
    

    我注意到我可以根据“找到你!”中的值使用断点捕获“跳转”。检查。基本上,当您到达第 9 行并尝试向上滚动时,每当“跳转”即将发生时都会发生两件事:

    • 您的 collectionViewcontentOffset 会根据我的 collectionView.contentSize.height - collectionView.frame.height 中的值进行更新我猜测是由于框架的变化
    • 由于某种原因,您的 collectionViewcontentSize 高度也大幅下降/上升(我说的是 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:

  1. As you scroll your collectionView from CustomViewController up/down, you are using the updated scroll offset through ViewControllerChildDelegate to adjust pagerViewTopAnchorConstraint.

  2. Adjusting this constraint also adjusts the height of your pageViewController which in turn adjusts your CustomViewController's collectionView height

  3. Adjusting the collectionView's height can cause jumps in its contentOffset depending on its current offset. Here's an answer which explains this: https://stackoverflow.com/a/21787754/9293498

    Just 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'

  4. 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's contentOffset as such:

     contentSizeObserver = collectionView.observe(\.contentSize) { [weak self] collectionView, _ in
         //Ignore the values on "Found you!" checks, as its relative to my device
         if collectionView.contentSize.height < 3000 {
             print("Found you!")
         }
         print("CONTENT SIZE SET: ", collectionView.contentSize)
     }
    
     contentOffsetObserver = collectionView.observe(\.contentOffset) { [weak self] collectionView, _ in
         //Ignore the values on "Found you!" checks, as its relative to my device
         if collectionView.contentOffset.y < 1460 {
             print("Found you!")
         }
         print("CONTENTOFFSET SET: ", collectionView.contentOffset)
     }
    

    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:

    • Your collectionView's contentOffset gets updated based on the value from collectionView.contentSize.height - collectionView.frame.height which I'm guessing is attributed to the frame change
    • For some reason, your collectionView's contentSize also gets a massive drop/rise in height (I'm talking 1000s) which is far from your pager'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.

活雷疯 2025-01-22 14:59:56

查看提供的视频,问题似乎是由于可重复使用单元的实施造成的。

当您第一次启动应用程序时,将有 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 issue

Hope this explanation helps :)

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文