【问题标题】:UIScrollView within a UIScrollView, how to keep a smooth transition when scrolling between the twoUIScrollView内的UIScrollView,如何在两者之间滚动时保持平滑过渡
【发布时间】:2025-12-16 11:05:02
【问题描述】:

我有以下布局(见下文),在大多数情况下都可以正常工作。用户可以在蓝色 UIScrollView 内滚动(出于所有意图和目的,它是 UITableView,但这个问题概括了这一点),然后当他们到达此滚动视图的末尾时,他们可以开始滚动再次(他们必须松开手指,然后再次打开,否则内部滚动视图会出现橡皮筋),“超级”滚动视图开始滚动,显示图像的其余部分。

这是我不想要的整个(他们必须取下手指,然后再打开,否则内部滚动视图会出现橡皮筋)。理想情况下,一旦包含的UIScrollView 到达其内容的末尾,我希望超级视图立即接管滚动,以便内部滚动视图不会橡皮筋。

当用户向上滚动时也是如此;当红色滚动视图到达其内容的顶部时,我希望内部蓝色滚动视图立即开始向上滚动,而不是顶部的红色滚动视图橡皮筋。

知道怎么做吗?我能够确定滚动视图何时到达其内容的末尾,但我不确定如何应用这些知识来达到我想要的效果。谢谢。

// Inner (blue) scroll view bounds checking
if (scrollView.contentOffset.y + scrollView.frame.size.height > scrollView.contentSize.height) { ... }

// Outer (red) scroll view bounds checking
if (scrollView.contentOffset.y < 0) { ... }

【问题讨论】:

    标签: objective-c ios uitableview uiscrollview


    【解决方案1】:

    天哪。 jackslash,你救了我的命。

    就我而言,我需要使用三个深度的滚动视图。

    1. 父滚动视图
      • 作为子级之一具有滚动视图
    2. 子滚动视图:在下图中显示为“说明滚动视图/查看表视图/信息滚动视图”)
    3. 隐藏的滚动视图
      • 将自己的内容偏移量分配给“父滚动视图”和“子滚动视图”
      • 具有整个展平内容的内容大小

    Screenshot of view hierarchy

    设置视图层次结构后,我只需要同步整个展平内容大小并正确分配内容偏移量。

        Observable.combineLatest(
            overviewStackView.rx.observe(CGRect.self, #keyPath(UIStackView.frame)).unwrap(),
            explanationScrollView.rx.observe(CGSize.self, #keyPath(UIScrollView.contentSize)).unwrap()
          )
          .subscribe(onNext: { [weak self] overviewStackViewFrame, explanationScrollViewContentSize in
            guard let self = self else { return }
            let totalContentHeight = overviewStackViewFrame.height + self.segmentedControl.frame.height + explanationScrollViewContentSize.height
            self.hiddenScrollView.contentSize.height = totalContentHeight
          })
          .disposed(by: disposeBag)
    
        hiddenScrollView.rx.didScroll
          .subscribe(onNext: { [weak self] _ in
            guard let self = self else { return }
            let currentHiddenScrollViewOffsetY = self.hiddenScrollView.contentOffset.y
            let parentScrollViewMaxOffsetY = self.overviewStackView.frame.height
            let expectedChildScrollViewOffsetY = max(currentHiddenScrollViewOffsetY - parentScrollViewMaxOffsetY, 0)
            self.parentScrollView.contentOffset.y = min(parentScrollViewMaxOffsetY, currentHiddenScrollViewOffsetY)
            self.explanationScrollView.contentOffset.y = expectedChildScrollViewOffsetY
          })
          .disposed(by: disposeBag)
    

    【讨论】:

      【解决方案2】:

      是的。我为你准备了一个绝妙的技巧。

      不要让你的红色轮廓视图变成一个滚动视图,而是让它成为一个填满屏幕的普通 UIView。在该视图中布置滚动视图(表格视图)和图像视图,就像它们在插图中一样。

      在所有其他滚动视图和图像视图上方放置一个填充根视图边界(即也填充屏幕)的滚动视图。将此视图的内容大小设置为要滚动的所有视图的总内容高度。换句话说,在所有其他视图之上有一个不可见的滚动视图,其内容大小高度是内部滚动视图(表格视图)内容大小高度 + 图像视图大小高度。

      层次结构应如下所示:

      然后,您使用非常高的内容大小制作的顶部滚动视图使其代表成为您的视图控制器。实施scrollViewDidScroll,我们将发挥一些作用。

      Scrollviews 基本上是通过使用时髦的动量公式调整边界原点来滚动的。所以在我们的scrollviewDidScroll 方法中,我们将简单地调整底层视图的边界:

      -(void)scrollViewDidScroll:(UIScrollView *)scrollView{
      
          //the scroll view underneath (in the container view) will have a max content offset equal to the content height
          //but minus the bounds height
          CGFloat maxYOffsetForUnderScrollView = self.underScrollView.contentSize.height - self.underScrollView.bounds.size.height;
      
          CGRect scrolledBoundsForContainerView = self.view.bounds;
      
          if (scrollView.contentOffset.y <= maxYOffsetForUnderScrollView) {
              //in this scenario we are still within the content for the underScrollView
              //so we make sure the container view is scrolled to the top and set the offset for the contained scrollview
              self.containerView.bounds = scrolledBoundsForContainerView;
              self.underScrollview.contentOffset = scrollView.contentOffset;
              return;
          }
      
          //in this scenario we have scrolled throug the entirety of the contained scrollview
          //set its offset to the max and change the bounds of the container view to scroll everything else.
          self.underScrollView.contentOffset = CGPointMake(0, maxYOffsetForUnderScrollView);
      
          scrolledBoundsForContainerView.origin.y = scrollView.contentOffset.y - maxYOffsetForUnderScrollView;
          self.containerView.bounds = scrolledBoundsForContainerView;
      }
      

      您会发现,由于每一帧动画都调用了 scrollViewDidScroll,因此包含视图的这种虚假滚动看起来非常自然。

      但是等等!我听你说。顶部的滚动视图现在拦截所有触摸,并且它下面的视图也需要被触摸。我对此也有一个有趣的解决方案。

      将顶部的滚动视图设置为在屏幕外某处(即,将其框架设置为屏幕外,但大小仍然相同。)然后在您的 viewDidLoad 方法中将滚动视图的 panGestureRecogniser 添加到主视图。这意味着您无需在屏幕上实际看到视图即可获得所有 iOS 自然滚动动力和内容。包含的滚动视图现在可能会变得不稳定,因为它的平移手势识别器也会被调用(它们的工作方式与 UIEvent 处理不同),因此您需要将其删除。

      - (void)viewDidLoad
      {
          [super viewDidLoad];
          // Do any additional setup after loading the view, typically from a nib.
      
          [self.view addGestureRecognizer:self.scrollview.panGestureRecognizer];
      
          [self.underScrollview removeGestureRecognizer:self.underScrollView.panGestureRecognizer];
      
          //further code to set up content sizes and stuff
      }
      

      我做这个很有趣,所以这里有一个指向 github 上示例项目的链接: https://github.com/joelparsons/multipleScrollers

      编辑:

      要在顶部滚动视图离开屏幕时显示滚动条,无论您将它放在哪里,您都可以将 scrollIndicatorInsets 设置为这样创建的插图:

      CGPoint scrollviewOrigin = self.scrollview.frame.origin;
      self.scrollview.scrollIndicatorInsets = UIEdgeInsetsMake(-scrollviewOrigin.y,0,scrollviewOrigin.y,scrollviewOrigin.x);
      

      *警告滚动视图仍然必须是正确的高度,但我相信你明白了。

      然后要使条形图在滚动视图的可见边界之外绘制,您必须关闭剪辑到边界

      self.scrollview.clipsToBounds = NO;
      

      【讨论】:

      • 出色的工作,非常感谢。有一个轻微的打嗝;当滚动视图被推离屏幕时,右侧的滚动指示器已变为便携式。有没有办法保持滚动指示器,以及允许下面的元素像以前一样获得触摸事件?
      • 同时,我将滚动视图的大小缩小到 8 的宽度,并让它紧贴屏幕的右侧。这似乎是一个肮脏的黑客,但我现在会接受你的回答,并希望你能有一个解决方案:)
      • 谢谢。您的解决方案还不错,但我用替代解决方案更新了我的答案,我将更新 github 代码
      • 哇,这值得+100!我被困在一个不同的问题上,但需要 3 天的类似解决方案!您应该考虑写一篇关于该技术的文章。
      • 史诗般的答案。非常感谢