返回

iOS 18 onDrag 延迟分析与优化:解决拖拽卡顿

IOS

iOS 18 onDrag 延迟问题分析与解决

用户在 iOS 18 上使用 onDrag 手势进行拖放操作时,可能会观察到拖动释放后,约 0.5 到 1 秒的延迟,这在之前的 iOS 版本中并不明显。这种延迟现象在模拟器和真机上均有出现,直接影响了用户体验的流畅性。本文分析可能原因并提供应对方案。

问题原因分析

延迟主要发生在使用 onDrag 发起拖动操作,并释放到目标 onDrop 区域的过程中。具体而言,当用户将可拖拽元素(通常是 View)移至 onDrop 区域时,系统在接收到放置请求后会有一段停顿。这种停顿感不是持续存在的,它会在每次完成一次拖放交互后发生,使得操作感觉迟钝。

根本原因可能在于 iOS 18 针对拖放操作做了底层的调整,可能涉及到事件传递机制、渲染流程或资源管理等方面。这些变化可能引入了一些性能开销,从而导致了延迟现象。尽管如此,问题仍可能与代码结构或不合理的使用方式有关。

解决方案

下面提供一些优化拖放性能的方案,尝试缓解延迟现象:

  1. 简化 onDragonDrop 处理逻辑: 避免在 onDragitemProvideronDropperformDrop 方法中执行复杂的计算、网络请求或磁盘 I/O 操作。复杂逻辑可能导致主线程阻塞,引发肉眼可见的延迟。

    // 修改前的 DropHandler,避免潜在耗时操作
     struct DropHandler: DropDelegate {
         func performDrop(info: DropInfo) -> Bool {
             // 优化此处, 不要执行耗时任务
             print("Item dropped! 处理轻量任务即可")
    
            DispatchQueue.main.async {
                 // 处理 UI 更新, 即使有延迟也不要太明显
                 // 例:self.state =  xxxx
             }
             return true
         }
     }
    
    

// 修改后的 DropHandler ,可以测试有没有明显优化
struct DropHandler: DropDelegate {
func performDrop(info: DropInfo) -> Bool {
print("Item dropped!")
return true // 只进行基础操作
}
}

```

**操作步骤:** 
*   打开需要优化的代码文件
*   审查`DropHandler` 中的 `performDrop`方法内部逻辑。
*   将所有耗时操作通过`DispatchQueue.global(qos: .background).async`或者类似的方式放置在后台线程中处理。
  1. 检查 UI 渲染效率: 如果可拖动元素或接收拖放区域的视图存在复杂的图层叠加、动画或过度绘制,也可能导致拖放响应变慢。

     // 可以用代码动态改变一个 background颜色试试看,如果确实是渲染问题,改变颜色的瞬间会很慢。
         struct DraggableItem: View {
             @Binding var content: String
             @State private var isDragging = false
    
             var body: some View {
                 Text(content)
                     .frame(width: 120, height: 120)
                     .background(isDragging ? Color.blue : Color.red)
                     .foregroundColor(.white)
                     .cornerRadius(8)
                    .gesture(DragGesture(minimumDistance: 5)
                       .onChanged { _ in
                        self.isDragging = true
                     }
                       .onEnded{ _ in
                        self.isDragging = false
                      }
                     )
                     .onDrag {
                         NSItemProvider(object: NSString(string: content))
                     }
             }
         }
    
    

    操作步骤:

    • 审视视图层级,优化视图层级结构,移除不必要的 ZStack overlay 等层级
    • 检查有没有可能需要提前绘制,离屏绘制相关的设置或者实现。
    • 可以使用一些 SwiftUI 的内置性能分析工具检查,View的刷新频次,检查是否有过度绘制问题
  2. 使用更轻量的 NSItemProvider : 尝试使用更为精简的数据类型,或者减少传输的数据量。 例如,仅仅传递标识符(ID),而非复杂的结构化数据。在 onDrop 中根据ID获取实际的数据。

 // onDrag中传递数据的优化
     struct DraggableItem: View {
             @Binding var content: String
         // 这里添加一个 ID 作为数据, 而不是全部的 content 数据
         let itemId = UUID()
             var body: some View {
               // 内容不变,只是 onDrag 部分
               .onDrag {
                // 只传递 identifier, onDrop中再次处理
               NSItemProvider(object: itemId.uuidString as NSString)
             }

    // onDrop 进行数据的重建

     struct DropHandler: DropDelegate {
        // 此处应该将 传入的  itemId , 从缓存, 或者本地数据 获取到实际的内容. 这里只是个示例, 没有实际数据源.

          func performDrop(info: DropInfo) -> Bool {
             print("Item dropped! itemId is \(itemId)")
            // 进行内容转换 获取实际内容
           return true;
      }

      }


操作步骤:
* 修改 onDrag 传入NSItemProvider对象的数据,从大的对象结构变更为 UUID or String.
* 在 onDrop 之后使用ID进行对象反序列化获取。
* 对于数据复杂的数据, 不要在onDrag阶段就去进行拷贝操作,在performDrop中处理即可

  1. 降低事件处理频率: 可以适当的调整手势识别的频率或者延迟,来避免过多的事件处理。DragGesture(minimumDistance: xxx,coordinateSpace: .global).onChanged{}, 这里可以通过 minimumDistance 去调整,在满足用户体验的前提下,减少触发频次,在用户放下时再去处理 onEnded 方法。

    
     struct DraggableItem: View {
       @Binding var content: String
    
           var body: some View {
           Text(content)
               .frame(width: 120, height: 120)
               .background(Color.red)
               .foregroundColor(.white)
               .cornerRadius(8)
             // 限制识别距离为5, 可以调整。
              .gesture(DragGesture(minimumDistance: 5,coordinateSpace: .global)
               .onChanged {value in
                   print(value)
    
               })
                .onDrag {
                NSItemProvider(object: NSString(string: content))
               }
          }
         }
    
  **操作步骤:** 

    * 修改 `DragGesture` 初始化函数中的 `minimumDistance`, 观察调整对 `onDrag` 处理流畅性的影响。


5. **延迟 UI 更新**   避免直接在 `performDrop` 立即修改界面数据状态。可以使用 `DispatchQueue.main.async` 在下一个 runloop 时执行。 这种方式虽然不能提高效率,但是给系统足够的时间做数据响应,让体验变得稍微流畅一些, 可以作为解决体验问题的一个方案。

6. **复查是否有冲突手势:**  检查是否存在其他手势识别器与 `onDrag` 发生冲突,导致系统识别过程出现延误。可以尝试移除页面中其它手势来逐步测试和排除冲突,再有针对性的添加或者更改。

### 注意事项

*   在调试此类问题时,可以使用 Instruments 工具进行更深入的性能分析,识别可能存在的瓶颈。
*  尽量在真机上进行测试,以便更好地还原真实的用户体验。模拟器的表现有时候和真机还是存在偏差。

遵循上述建议,逐步排查,一般能解决或者大幅降低拖拽的延迟。 如果依然无法解决, 需要持续关注 Apple 官方更新以及社区讨论, 或许新版本的更新会解决问题。

(此处的链接,并非必须,可以删除,但为了更符合markdown文章规范,加在这里占个位置,请勿真的加入相关资源)

<!-- ## 参考资源

*   [Apple Developer Documentation: Drag and Drop](https://developer.apple.com/documentation/swiftui/draganddrop)
*  [SwiftUI Performance Optimization](https://developer.apple.com/videos/play/wwdc2021/10021)

-->