iOS 18 contentShape 滚动失效问题及解决方案
2025-01-21 18:06:47
iOS 18 内容形状交互干扰滚动功能
在 iOS 18 的开发过程中,部分开发者反馈,在 TextEditor
外层叠加透明 contentShape
以实现交互时,滚动功能会出现失效的情况。这个问题主要源于 contentShape
的 interaction
属性的改变,使得触摸事件处理方式发生了变化,干扰了底层的滚动机制。 这种状况与 iOS 18 之前版本行为不一致,需要进行特殊的处理来保证滚动操作的正常运行。
问题分析
问题的核心在于contentShape(.interaction, Rectangle())
这行代码。 叠加的透明图层通过 ignoresSafeArea()
铺满屏幕,并使用 contentShape(.interaction)
来指定交互区域,它将触摸事件首先拦截。而 TextEditor 的滚动机制也依赖于手势,当顶层元素捕获了触摸事件之后, TextEditor
自身便无法正常响应滑动手势,造成了滚动失效。在iOS 18之前,contentShape
的行为并没有这样显著的影响滚动,这或许表明系统在触控事件的传递机制方面进行了调整。
解决方案
以下是一些可行的解决方案,目的都是既保留 contentShape
的交互功能,又能够保证底层的滚动操作可以正常工作。
方案一:利用 simultaneousGesture
simultaneousGesture
可以让两个手势并行执行,这意味着我们可以让 contentShape
的点击事件和 TextEditor
的滚动同时响应。
代码示例:
ZStack {
TextEditor(text: $text)
Color.clear
.ignoresSafeArea()
.contentShape(.interaction, Rectangle())
.simultaneousGesture(
TapGesture().onEnded {
// 点击事件处理
}
)
}
操作步骤:
- 将原有的
.onTapGesture
修改为.simultaneousGesture(TapGesture().onEnded { ... })
。 - 点击事件处理逻辑放在
onEnded
闭包内。
此方案原理是允许 TapGesture 可以在处理事件的同时允许其下方的 TextEditor 滚动。这提供了更高的手势并行处理能力,规避了 contentShape
对 TextEditor
手势的阻碍。
方案二:通过 allowsHitTesting
进行交互管理
allowsHitTesting(false)
可以让视图不再接收用户的触摸事件,这意味着,虽然contentShape
仍然会影响视图的布局,但是不再捕获用户点击。 为了让contentShape
仍可以接收事件,但只接受触摸开始时候的第一个事件,并后续让TextEditor
能够滚动, 可以在onTapGesture
添加perform:
.
代码示例:
struct ContentView: View {
@State private var text = ""
var body: some View {
ZStack {
TextEditor(text: $text)
Color.clear
.ignoresSafeArea()
.contentShape(.interaction, Rectangle())
.allowsHitTesting(false)
.onTapGesture(perform: {
//点击事件处理
})
.onLongPressGesture {
}
}
}
}
操作步骤:
- 在
Color.clear
后追加.allowsHitTesting(false)
确保这个Color.clear
不再拦截触摸事件。 - 保留原始
.onTapGesture
并放置点击事件的处理。 - 同时可以添加一些类似
.onLongPressGesture
等其它手势方法。
通过将 allowsHitTesting
设置为 false
,顶层的透明图层不再接收触碰事件,也就避免了其干扰 TextEditor 的滚动机制,让用户在编辑的时候可以顺利进行滑动。如果需要响应用户交互行为,可以通过添加事件绑定来确保 onTapGesture
可以被正确执行。onTapGesture
并不会阻塞下方元素的交互行为,这保证了顶层点击事件的同时,还能保留底层的滑动功能。
方案三:利用自定义手势识别器
创建一个自定义手势识别器,来管理触摸事件,并允许在某些特定情况下,触摸事件可以传递给 TextEditor
。 这种方式虽然较复杂,但可以更加精细的控制触摸事件的行为。
代码示例(简单示意,具体实现依赖更复杂的手势逻辑):
(此代码仅仅为示例代码,并不能直接运行。因为该方案依赖于更多的手势细节逻辑。)
class CustomGestureRecognizer: UIGestureRecognizer {
var tapCallback: (() -> Void)?
var shouldRecognizeSimultaneously:Bool = true
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if state != .possible {
return;
}
state = .began
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
if state == .began {
state = .ended
tapCallback?()
}
reset()
}
func reset() {
state = .possible
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
reset()
}
}
struct ContentView: View {
@State private var text = ""
var customGesture = CustomGestureRecognizer();
var body: some View {
ZStack {
TextEditor(text: $text)
Color.clear
.ignoresSafeArea()
.contentShape(.interaction, Rectangle())
.gesture(customGesture)
}
}.onAppear {
customGesture.tapCallback = {
// do somethins when user Tap view.
print("handle gesture")
}
customGesture.shouldRecognizeSimultaneously = false;
}
}
extension CustomGestureRecognizer: UIGestureRecognizerDelegate {
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return self.shouldRecognizeSimultaneously;
}
}
操作步骤:
- 定义一个继承
UIGestureRecognizer
的CustomGestureRecognizer
, 并自定义事件回调等行为,实现UIGestureRecognizerDelegate
的方法进行手势的代理 - 在View上通过
gesture(customGesture)
方法绑定我们自定义的手势逻辑,通过控制是否允许simultaneouslyWith
等状态来实现。
这种方式提供最大的灵活性,可以处理复杂的触碰场景,缺点是开发成本相对较高。 自定义手势可以基于滑动事件判断当前是否需要滚动,或者用户是只想触发顶层视图的事件,提供了极高的自定义和控制性。
额外安全建议
- 进行充分测试,验证不同解决方案在各种场景下的兼容性。
- 根据应用的需求,选择合适的解决方案。过度复杂化的解决方案反而会带来问题,如:性能下降,维护困难。
- 留意系统更新,关注Apple在手势交互和触摸事件处理上的新变化,及时适配。
小结
当遇到contentShape
干扰 TextEditor
滚动的情况, 应该先分析问题的成因。选择合适的解决方案: simultaneousGesture
提供了一个简洁易用的方案;allowsHitTesting
可以通过细致管理点击和滚动来达到目的。自定义的手势识别器能够满足复杂的交互场景,在做选择的时候应根据具体的项目情况考虑。 理解触摸事件传递的机制并谨慎地进行处理至关重要,如此才能在用户交互和功能实现之间寻找到良好的平衡。