这是我参与更文挑战的第4天,活动详情查看: 更文挑战
UIKit 中,当一个 scrollView(或其子类)的父控件中包含 scrollView 或其子类时,也就是说嵌套 ScrollView 时,当 scrollView 滑动到顶部或者底部时,再向上/向下滑动时,scrollView 类型的父控件会其联动。
这在大多数的情况下,保持了动画的连续性。但是有些情况需要禁止这种联动,比如直播类的 App,最外层为一个 TableView 在展示正在直播的直播间,在直播间中还会有一个消息展示的 tableView,此时就需要禁止滚动 IM 消息时禁止外层的 tableView 滚动。
如何取消这种联动呢?
方案一:子 ScrollView 开始滚动和停止时发送通知(失败)
我首先想到的是监听子 scrollView 的滚动事件,当子类开始滚动和结束滚动时发送通知,修改父 ScrollView 的 isScrollEnabled
属性,解除滚动。代码如下:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
// 发送开始滚动通知,让父类禁止滚动
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
let scrollToScrollStop = !scrollView.isTracking && !scrollView.isDragging && !scrollView.isDecelerating
if (scrollToScrollStop) {
didEndScroll()
}
}
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
if (!decelerate) {
let scrollToScrollStop = !scrollView.isTracking && !scrollView.isDragging && !scrollView.isDecelerating;
if (scrollToScrollStop) {
didEndScroll()
}
}
}
func didEndScroll() {
// 发送停止滚动通知,让父类开始滚动
}
复制代码
但是发现当在子 ScrollView 的可滚动的边界时,继续滚动,并不会触发 scrollViewDidScroll
方法,所以这种方案不可行。
方案二:重写子 ScrollView 的 hitTest 方法
因为父控件中的每次事件都会通过 hitTest
传递到子控件,所以可以通过响应者链,获取父 scrollView 控件,修改其 isScrollEnabled
属性,解除联动。
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
var responder = next
// 父控件为 scrollView 的控件
var superScrollView: UIScrollView?
// 通过 next 响应者向上寻找父控件为 scrollView 的控件
// 当 superScrollView != nil 时,说明按照条件获取到 scrollView 或其子类的父控件对象
while responder != nil && superScrollView == nil {
if let scrollView = responder as? UIScrollView {
superScrollView = scrollView
} else {
responder = responder?.next
}
}
let view = super.hitTest(point, with: event)
// 如果 view != nil,说明手势操作是当前的 scrollView/tableView 发出的,
// 则需要需要禁止父控件 superScrollView 的滚动;
// 否则相反。
if view != nil {
superScrollView?.isScrollEnabled = false
} else {
superScrollView?.isScrollEnabled = true
}
return view
}
复制代码
通过实验发现这种方案可行。
其他
在修改 tableView 的 isScrollEnabled
时,发现会修改 tableView 的 contentOffset
,造成 tableView 偏移量抖动。这个是因为自动设置 scrollView 的内边距造成的,可以通过修改下面的属性关闭:
// iOS 11 以前,修改 ViewController 的属性
automaticallyAdjustsScrollViewInsets = false
// iOS 11 之后,修改 ScrollView 的属性
tableView.contentInsetAdjustmentBehavior = .never
复制代码