返回

SwiftUI旋转后便签拖动修复:两种有效方案

IOS

SwiftUI:旋转后便签移动不正确

在 SwiftUI 中实现可拖动、调整大小和旋转的便签功能时,一个常见的难题是在旋转便签后,其拖动行为与预期不符。 即使应用了看似正确的三角函数来调整位移,移动也经常会在旋转后的错误方向上发生。 这篇文章会探讨这种现象的原因并提供一些切实可行的解决方案。

问题分析

问题主要源于拖动手势的位移坐标系与旋转后便签的坐标系不匹配。原始的 DragGesture 位移值基于屏幕坐标系,但旋转后的便签则在其自身的局部坐标系中显示。当我们尝试直接使用屏幕坐标系的位移值来更新便签位置时,便签就会沿原始轴线(即旋转前的轴线)移动,导致旋转后出现视觉上的错误拖动方向。简单来说,我们在全局坐标系中处理局部位移,因而导致了不一致的移动。

adjustTranslationForRotation 函数的逻辑似乎是正确的,问题并不在于它的实现,而在于其调用时机以及我们如何理解 DragGesture 的返回值。它按照期望的逻辑将屏幕坐标系的偏移量调整为旋转后的坐标系偏移量,但此调整值与最后设置 offset 值并没有被合理应用。我们需要确保更新便签 position 变量时的偏移量使用的计算偏移。

解决方案

要解决这个问题,需要采取几个关键步骤:

  1. 确保 lastPosition 基于的是旋转后的坐标系。
  2. 使用正确调整后的位移值更新位置。

下面提供了详细步骤和对应的代码。

方案一:使用累计位移差值更新位置

步骤:

  1. 在每次 DragGesture.onChanged 回调中,计算当前拖动偏移量 (value.translation) 经旋转调整后的值 (adjustedTranslation)。
  2. adjustedTranslation 的变化量累积到 position 状态变量,而不是基于 lastPosition 进行重新计算 ,最后赋值更新 position 状态变量。
  3. .onEnded 回调中,直接使用当前 position 的值赋值给 lastPosition ,避免重复偏移量叠加。
    代码:

修改 TransformHelper.dragGesture 函数:

static func dragGesture(
    position: Binding<CGSize>,
    lastPosition: Binding<CGSize>,
    rotation: Angle,
    isResizing: Bool,
    isDragging: Binding<Bool>
) -> some Gesture {
    return DragGesture()
        .onChanged { value in
            if !isResizing {
                let adjustedTranslation = adjustTranslationForRotation(value.translation, rotation: rotation)
                 position.wrappedValue.width += adjustedTranslation.width
                 position.wrappedValue.height += adjustedTranslation.height
                
                isDragging.wrappedValue = true
            }
        }
        .onEnded { _ in
             if isDragging.wrappedValue {
                 isDragging.wrappedValue = false
                lastPosition.wrappedValue = position.wrappedValue
             }
        }
}

此方案通过累计拖动产生的位移变化,从而实现正确的移动方向。

方案二:使用拖动手势差值进行偏移调整

步骤:

  1. 在每次 .onChanged 中使用当前的 position 结合当前 lastPosition 并叠加当前的 adjustedTranslation 值来更新位置。
  2. .onEnded 中直接更新lastPosition

代码:

static func dragGesture(
    position: Binding<CGSize>,
    lastPosition: Binding<CGSize>,
    rotation: Angle,
    isResizing: Bool,
    isDragging: Binding<Bool>
) -> some Gesture {
    return DragGesture()
        .onChanged { value in
             if !isResizing {
                let adjustedTranslation = adjustTranslationForRotation(value.translation, rotation: rotation)

                position.wrappedValue = CGSize(
                        width: position.wrappedValue.width - lastPosition.wrappedValue.width + adjustedTranslation.width ,
                        height: position.wrappedValue.height - lastPosition.wrappedValue.height  + adjustedTranslation.height
                        )

               
                isDragging.wrappedValue = true
             }
          
        }
        .onEnded { _ in
            if isDragging.wrappedValue {
                isDragging.wrappedValue = false
                lastPosition.wrappedValue = position.wrappedValue
           
            }
       }
    
}

此方案通过维护相对差值来实现正确偏移,能够让在移动中,lastPosition 基于实时调整偏移的位置。

安全建议

  • 在处理用户输入时,应该总是对可能出现的异常情况进行处理。虽然这里仅仅是相对简单的位置偏移,依然需要对相关数据进行校验。
  • 避免过于复杂的三角函数运算,尤其是当涉及到动画或其他计算量密集的操作时。确保逻辑简洁高效,减少潜在的性能问题。
  • DragGesture 和其他 SwiftUI 的手势在嵌套使用的时候应该特别注意事件处理的传递,确保各事件监听能够正确的接收以及作出预期处理。
  • 对于复杂的自定义手势交互逻辑,考虑将其封装成独立的视图或者组件,这可以提高代码的可读性和可维护性。

这些解决方案能够有效地解决旋转后便签拖动不正确的问题,选择合适的方法,能够提高开发效率,并且让用户交互更加友好流畅。通过对核心问题的分析和解决,可以让我们更好地理解 SwiftUI 中手势交互的本质。