返回

iOS 18 contentShape 滚动失效问题及解决方案

IOS

iOS 18 内容形状交互干扰滚动功能

在 iOS 18 的开发过程中,部分开发者反馈,在 TextEditor 外层叠加透明 contentShape 以实现交互时,滚动功能会出现失效的情况。这个问题主要源于 contentShapeinteraction 属性的改变,使得触摸事件处理方式发生了变化,干扰了底层的滚动机制。 这种状况与 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 {
                        //  点击事件处理
                    }
                )
        }

操作步骤:

  1. 将原有的 .onTapGesture 修改为 .simultaneousGesture(TapGesture().onEnded { ... })
  2. 点击事件处理逻辑放在onEnded闭包内。

此方案原理是允许 TapGesture 可以在处理事件的同时允许其下方的 TextEditor 滚动。这提供了更高的手势并行处理能力,规避了 contentShapeTextEditor 手势的阻碍。

方案二:通过 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 {

             }
        }
      }
    }

操作步骤:

  1. Color.clear后追加.allowsHitTesting(false) 确保这个Color.clear不再拦截触摸事件。
  2. 保留原始 .onTapGesture并放置点击事件的处理。
  3. 同时可以添加一些类似.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;
      }
}

操作步骤:

  1. 定义一个继承UIGestureRecognizerCustomGestureRecognizer, 并自定义事件回调等行为,实现UIGestureRecognizerDelegate的方法进行手势的代理
  2. 在View上通过gesture(customGesture) 方法绑定我们自定义的手势逻辑,通过控制是否允许simultaneouslyWith等状态来实现。

这种方式提供最大的灵活性,可以处理复杂的触碰场景,缺点是开发成本相对较高。 自定义手势可以基于滑动事件判断当前是否需要滚动,或者用户是只想触发顶层视图的事件,提供了极高的自定义和控制性。

额外安全建议

  • 进行充分测试,验证不同解决方案在各种场景下的兼容性。
  • 根据应用的需求,选择合适的解决方案。过度复杂化的解决方案反而会带来问题,如:性能下降,维护困难。
  • 留意系统更新,关注Apple在手势交互和触摸事件处理上的新变化,及时适配。

小结

当遇到contentShape 干扰 TextEditor 滚动的情况, 应该先分析问题的成因。选择合适的解决方案: simultaneousGesture 提供了一个简洁易用的方案;allowsHitTesting 可以通过细致管理点击和滚动来达到目的。自定义的手势识别器能够满足复杂的交互场景,在做选择的时候应根据具体的项目情况考虑。 理解触摸事件传递的机制并谨慎地进行处理至关重要,如此才能在用户交互和功能实现之间寻找到良好的平衡。