学习目标

  • 理解SwiftUI中手势识别器的基本概念。
  • 掌握如何使用onTapGesture处理点击手势。
  • 学习如何使用DragGesture处理拖动手势。
  • 掌握如何使用MagnificationGesture处理缩放手势。
  • 掌握如何使用RotationGesture处理旋转手势。
  • 了解手势组合(simultaneouslysequencedexclusively)和手势状态。

学习内容

1. 手势识别器基础

SwiftUI通过手势识别器(Gesture Recognizers)来响应用户的交互。你可以将一个或多个手势识别器附加到任何视图上。

2. onTapGesture (点击手势)

用于检测单次或多次点击。

2.1 单次点击
struct TapGestureExample: View {
    @State private var message = "Tap me!"
    
    var body: some View {
        Text(message)
            .font(.largeTitle)
            .padding()
            .background(Color.blue)
            .foregroundColor(.white)
            .cornerRadius(10)
            .onTapGesture {
                message = "Tapped!"
            }
    }
}
2.2 多次点击
struct DoubleTapGestureExample: View {
    @State private var tapCount = 0
    
    var body: some View {
        Text("Tap Count: \(tapCount)")
            .font(.largeTitle)
            .padding()
            .onTapGesture(count: 2) {
                tapCount += 1
            }
    }
}

3. DragGesture (拖动手势)

用于检测视图的拖动操作,并提供拖动过程中的位置和速度信息。

3.1 基本拖动
struct DragGestureExample: View {
    @State private var offset = CGSize.zero
    
    var body: some View {
        Circle()
            .fill(Color.red)
            .frame(width: 100, height: 100)
            .offset(offset) // 应用偏移量
            .gesture(
                DragGesture()
                    .onChanged { gesture in
                        offset = gesture.translation // 实时更新偏移量
                    }
                    .onEnded { gesture in
                        // 拖动结束时可以做一些处理,例如回到原位或吸附到某个位置
                        // offset = .zero // 拖动结束后回到原位
                        print("Drag ended at: \(gesture.location)")
                    }
            )
    }
}
3.2 拖动状态

DragGestureonChangedonEnded闭包会接收一个DragGesture.Value参数,其中包含:

  • location: 当前手势在视图中的位置。
  • startLocation: 手势开始时的位置。
  • translation: 从开始位置到当前位置的累积偏移量。
  • predictedEndLocation: 预测手势结束时的位置。
  • velocity: 手势的速度。

4. MagnificationGesture (缩放手势)

用于检测捏合缩放操作。

struct MagnificationGestureExample: View {
    @State private var scale: CGFloat = 1.0
    @State private var lastScale: CGFloat = 1.0
    
    var body: some View {
        Image(systemName: "heart.fill")
            .font(.system(size: 100))
            .foregroundColor(.pink)
            .scaleEffect(scale)
            .gesture(
                MagnificationGesture()
                    .onChanged { value in
                        scale = lastScale * value // 实时更新缩放比例
                    }
                    .onEnded { value in
                        lastScale = scale // 记录最终缩放比例
                    }
            )
    }
}

5. RotationGesture (旋转手势)

用于检测两指旋转操作。

struct RotationGestureExample: View {
    @State private var angle: Angle = .zero
    @State private var lastAngle: Angle = .zero
    
    var body: some View {
        Rectangle()
            .fill(Color.green)
            .frame(width: 150, height: 150)
            .rotationEffect(angle)
            .gesture(
                RotationGesture()
                    .onChanged { value in
                        angle = lastAngle + value // 实时更新旋转角度
                    }
                    .onEnded { value in
                        lastAngle = angle // 记录最终旋转角度
                    }
            )
    }
}

6. 手势组合

SwiftUI提供了三种手势组合方式:

6.1 simultaneously(with:) (同时识别)

允许两个手势同时被识别。

struct SimultaneousGestureExample: View {
    @State private var offset = CGSize.zero
    @State private var scale: CGFloat = 1.0
    
    var body: some View {
        Image(systemName: "star.fill")
            .font(.system(size: 100))
            .offset(offset)
            .scaleEffect(scale)
            .gesture(
                DragGesture()
                    .onChanged { offset = $0.translation }
                    .simultaneously(with: MagnificationGesture()
                        .onChanged { scale = $0 }
                    )
            )
    }
}
6.2 sequenced(before:) (顺序识别)

一个手势成功后,才开始识别另一个手势。

struct SequencedGestureExample: View {
    @State private var message = "Long press then drag"
    
    var body: some View {
        Text(message)
            .font(.title)
            .padding()
            .gesture(
                LongPressGesture(minimumDuration: 1)
                    .onEnded { _ in
                        message = "Long pressed! Now drag."
                    }
                    .sequenced(before: DragGesture()
                        .onChanged { _ in
                            message = "Dragging..."
                        }
                        .onEnded { _ in
                            message = "Drag ended."
                        }
                    )
            )
    }
}
6.3 exclusively(before:) (排他识别)

如果第一个手势被识别,则第二个手势不会被识别;否则,尝试识别第二个手势。

struct ExclusiveGestureExample: View {
    @State private var message = "Tap or Long Press"
    
    var body: some View {
        Text(message)
            .font(.title)
            .padding()
            .gesture(
                TapGesture()
                    .onEnded { _ in
                        message = "Tapped!"
                    }
                    .exclusively(before: LongPressGesture(minimumDuration: 1)
                        .onEnded { _ in
                            message = "Long Pressed!"
                        }
                    )
            )
    }
}

7. 手势状态

手势通常有三种状态:

  • inactive:手势未被激活。
  • active:手势正在被识别(例如,拖动正在进行中)。
  • ended:手势识别完成。

你可以通过@GestureState属性包装器来跟踪手势的瞬时状态,当手势结束时,@GestureState变量会自动重置。

struct GestureStateExample: View {
    @GestureState private var isDetectingLongPress = false
    @State private var feedback = ""
    
    var body: some View {
        Text(feedback)
            .font(.largeTitle)
            .padding()
            .background(isDetectingLongPress ? Color.red : Color.blue)
            .foregroundColor(.white)
            .cornerRadius(10)
            .gesture(
                LongPressGesture(minimumDuration: 1)
                    .updating($isDetectingLongPress) { currentState, gestureState, transaction in
                        gestureState = currentState // 当长按开始时,gestureState变为true
                    }
                    .onEnded { _ in
                        feedback = "Long Press Detected!"
                    }
            )
    }
}

实践练习

  1. 可拖动的图片
    • 创建一个Image视图,使其可以通过拖动手势在屏幕上自由移动。
    • 尝试在拖动结束后,让图片平滑地回到屏幕中心。
  2. 图片缩放与旋转
    • 创建一个Image视图,使其可以通过捏合手势进行缩放,并通过两指旋转手势进行旋转。
    • 确保缩放和旋转可以同时进行。
  3. 自定义手势组合
    • 创建一个视图,当用户先长按(1秒)然后拖动时,显示不同的文本提示。
    • 如果用户只是长按,则显示“长按成功”,不进行拖动。

思考题

  • @GestureState和普通的@State在手势处理中的区别是什么?为什么@GestureState在某些场景下更优?
  • 如何处理手势冲突,例如一个视图既有点击手势又有拖动手势?
  • 在实际应用中,你会如何设计复杂的用户手势交互?
Logo

为武汉地区的开发者提供学习、交流和合作的平台。社区聚集了众多技术爱好者和专业人士,涵盖了多个领域,包括人工智能、大数据、云计算、区块链等。社区定期举办技术分享、培训和活动,为开发者提供更多的学习和交流机会。

更多推荐