基于本文回答

播面 播面

刷题像听歌,多听自然懂
0
评论

如何在 SwiftUI 中识别和处理手势(Tap, Drag, Magnification 等)?

知识点图片

在 SwiftUI 中,处理手势的核心修饰符是 .gesture()。你可以将各种手势结构体(如 TapGesture, DragGesture 等)传递给这个修饰符。

以下是 SwiftUI 中常用手势的识别与处理方法的详细指南。


1. 点击手势 (Tap Gesture)

这是最简单的手势。SwiftUI 提供了一个简便的修饰符 .onTapGesture,也可以使用完整的 TapGesture 来处理双击或多击。

基础单击

plaintext
Text("点击我")
    .padding()
    .background(Color.blue)
    .foregroundColor(.white)
    .cornerRadius(10)
    .onTapGesture {
        print("被点击了!")
    }

双击 (Double Tap)

plaintext
Image(systemName: "heart.fill")
    .resizable()
    .frame(width: 50, height: 50)
    .foregroundColor(.red)
    .gesture(
        TapGesture(count: 2) // 设置点击次数
            .onEnded {
                print("双击检测成功!")
            }
    )

2. 长按手势 (Long Press Gesture)

用于检测用户按住视图一段时间。

plaintext
struct LongPressView: View {
    @State private var isPressed = false
    
    var body: some View {
        Circle()
            .fill(isPressed ? Color.red : Color.green)
            .frame(width: 100, height: 100)
            .gesture(
                LongPressGesture(minimumDuration: 1.0) // 按住至少1秒
                    .onEnded { _ in
                        print("长按结束")
                        isPressed.toggle()
                    }
                    .onChanged { _ in
                        // 注意:LongPressGesture 的 onChanged 并不常用,
                        // 通常配合 onPressingChanged (iOS 14+) 使用
                    }
            )
            // iOS 14+ 推荐方式:监听按压状态变化
            .onLongPressGesture(minimumDuration: 1.0, pressing: { pressing in
                // pressing 为 true 表示正在按住,false 表示松开
                if pressing {
                    print("开始按压...")
                } else {
                    print("按压取消或完成")
                }
            }, perform: {
                print("长按动作触发!")
            })
    }
}

3. 拖拽手势 (Drag Gesture)

用于移动视图或获取滑动的偏移量。通常配合 @State@GestureState 使用。

plaintext
struct DragView: View {
    @State private var offset = CGSize.zero
    @State private var lastOffset = CGSize.zero // 记录上一次结束的位置

    var body: some View {
        RoundedRectangle(cornerRadius: 20)
            .fill(Color.orange)
            .frame(width: 100, height: 100)
            .offset(x: offset.width, y: offset.height)
            .gesture(
                DragGesture()
                    .onChanged { value in
                        // value.translation 是从手势开始时的总位移
                        self.offset = CGSize(
                            width: lastOffset.width + value.translation.width,
                            height: lastOffset.height + value.translation.height
                        )
                    }
                    .onEnded { value in
                        // 手势结束,保存当前位置
                        self.lastOffset = self.offset
                    }
            )
    }
}

4. 缩放手势 (Magnification Gesture)

用于双指捏合缩放。

plaintext
struct ZoomView: View {
    @State private var currentScale: CGFloat = 1.0
    @State private var finalScale: CGFloat = 1.0

    var body: some View {
        Image(systemName: "photo")
            .resizable()
            .aspectRatio(contentMode: .fit)
            .frame(width: 200, height: 200)
            .scaleEffect(currentScale * finalScale)
            .gesture(
                MagnificationGesture()
                    .onChanged { value in
                        // value 是缩放倍数 (例如 1.5)
                        currentScale = value
                    }
                    .onEnded { value in
                        finalScale *= currentScale
                        currentScale = 1.0
                    }
            )
    }
}

5. 旋转手势 (Rotation Gesture)

用于双指旋转视图。

plaintext
struct RotateView: View {
    @State private var currentAngle = Angle.zero
    @State private var finalAngle = Angle.zero

    var body: some View {
        Rectangle()
            .fill(Color.purple)
            .frame(width: 150, height: 150)
            .rotationEffect(currentAngle + finalAngle)
            .gesture(
                RotationGesture()
                    .onChanged { angle in
                        currentAngle = angle
                    }
                    .onEnded { angle in
                        finalAngle += currentAngle
                        currentAngle = .zero
                    }
            )
    }
}

6. 高级技巧:使用 @GestureState

@GestureState 是处理手势的利器。它的特点是:当手势结束时,它会自动重置为初始值。这非常适合制作“松手回弹”的效果。

plaintext
struct GestureStateView: View {
    // 拖拽时更新 offset,松手后自动变回 CGSize.zero
    @GestureState private var dragOffset = CGSize.zero

    var body: some View {
        Circle()
            .fill(Color.pink)
            .frame(width: 100, height: 100)
            .offset(dragOffset)
            .gesture(
                DragGesture()
                    .updating($dragOffset) { value, state, transaction in
                        // value: 手势当前数据
                        // state: 对应的 dragOffset 属性 (inout)
                        // transaction: 动画事务
                        state = value.translation
                    }
            )
            .animation(.spring(), value: dragOffset) // 添加回弹动画
    }
}

7. 组合手势 (Composing Gestures)

SwiftUI 允许你将多个手势组合在一起。主要有三种组合方式:

  1. Simultaneously (同时进行): 例如一边拖拽一边缩放。
  2. Sequenced (顺序进行): 例如先长按,然后才能拖拽。
  3. Exclusive (排他): 两个手势竞争,只有一个能生效。

示例:同时缩放和旋转

plaintext
struct CombinedGestureView: View {
    @State private var scale: CGFloat = 1.0
    @State private var angle: Angle = .zero

    var body: some View {
        let magnification = MagnificationGesture()
            .onChanged { v in scale = v }
            .onEnded { _ in scale = 1.0 }

        let rotation = RotationGesture()
            .onChanged { a in angle = a }
            .onEnded { _ in angle = .zero }

        // 使用 .simultaneously 组合
        let combined = magnification.simultaneously(with: rotation)

        Rectangle()
            .fill(Color.blue)
            .frame(width: 150, height: 150)
            .scaleEffect(scale)
            .rotationEffect(angle)
            .gesture(combined)
    }
}

总结与注意事项

  1. 点击区域 (contentShape): 如果你给一个透明的视图(如 SpacerColor.clear)添加手势,默认可能无法触发。你需要添加 .contentShape(Rectangle()) 来定义可点击区域。
  2. 冲突处理:ScrollViewList 中使用 DragGesture 可能会与滚动手势冲突。通常需要根据优先级调整。
  3. 状态更新: onChanged 会非常频繁地调用,尽量避免在其中进行过重的计算。
00:00
00:00