《SwiftUI 进阶学习第3章:手势与交互》

7 阅读5分钟

手势基础

在 SwiftUI 中,手势是通过各种手势类型和修饰符来实现的,它们可以附加到任何视图上,以响应用户的交互。


常用手势类型

1. 点击手势

点击手势通过 onTapGesture 修饰符实现,用于检测用户的点击操作。

示例代码:

@State private var isTapped = false

Rectangle()
    .fill(.blue)
    .frame(width: 200, height: 100)
    .cornerRadius(10)
    .onTapGesture {
        isTapped.toggle()
    }

点击次数:

可以通过 count 参数指定点击次数,例如双击:

Rectangle()
    .onTapGesture(count: 2) {
        tapCount += 1
    }

2. 长按手势

长按手势通过 onLongPressGesture 修饰符实现,用于检测用户的长按操作。

示例代码:

@State private var isLongPressed = false

Rectangle()
    .fill(.green)
    .frame(width: 200, height: 100)
    .cornerRadius(10)
    .onLongPressGesture {
        isLongPressed.toggle()
    }

3. 拖拽手势

拖拽手势通过 DragGesture 实现,用于检测用户的拖拽操作。

示例代码:

@State private var dragOffset = CGSize.zero

Circle()
    .fill(.red)
    .frame(width: 50, height: 50)
    .offset(dragOffset)
    .gesture(
        DragGesture()
            .onChanged { value in
                dragOffset = value.translation
            }
            .onEnded { value in
                // 可以在这里添加结束拖动的逻辑
            }
    )

4. 缩放手势

缩放手势通过 MagnificationGesture 实现,用于检测用户的缩放操作。

示例代码:

@State private var scale = 1.0

Rectangle()
    .fill(.purple)
    .frame(width: 200, height: 200)
    .cornerRadius(10)
    .scaleEffect(scale)
    .gesture(
        MagnificationGesture()
            .onChanged { value in
                scale = value
            }
    )

5. 旋转手势

旋转手势通过 RotationGesture 实现,用于检测用户的旋转操作。

示例代码:

@State private var rotation = 0.0

Rectangle()
    .fill(.orange)
    .frame(width: 200, height: 200)
    .cornerRadius(10)
    .rotationEffect(.degrees(rotation))
    .gesture(
        RotationGesture()
            .onChanged { value in
                rotation = value.degrees
            }
    )

组合手势

组合手势是将多种手势效果结合在一起,可以使用 .simultaneousGesture() 修饰符。

示例代码:

@State private var offset = CGSize.zero
@State private var scale = 1.0
@State private var rotation = 0.0

Rectangle()
    .fill(.blue)
    .frame(width: 200, height: 200)
    .cornerRadius(10)
    .offset(offset)
    .scaleEffect(scale)
    .rotationEffect(.degrees(rotation))
    .gesture(
        DragGesture()
            .onChanged { value in
                offset = value.translation
            }
    )
    .simultaneousGesture(
        MagnificationGesture()
            .onChanged { value in
                scale = value
            }
    )
    .simultaneousGesture(
        RotationGesture()
            .onChanged { value in
                rotation = value.degrees
            }
    )

手势状态管理

手势通常与状态管理结合使用,以追踪手势的状态和数据。

示例代码:

@State private var isDragging = false

Rectangle()
    .fill(.purple)
    .frame(width: 100, height: 100)
    .cornerRadius(10)
    .gesture(
        DragGesture()
            .onChanged { _ in
                isDragging = true
            }
            .onEnded { _ in
                isDragging = false
            }
    )

实践示例:完整手势演示

以下是一个完整的手势演示示例,包含了各种手势类型和组合:

import SwiftUI

struct GestureAndInteractionDemo: View {
    // 状态管理
    @State private var isTapped = false
    @State private var isLongPressed = false
    @State private var offset = CGSize.zero
    @State private var scale = 1.0
    @State private var rotation = 0.0
    @State private var dragOffset = CGSize.zero
    @State private var isDragging = false
    @State private var tapCount = 0
    
    var body: some View {
        ScrollView {
            VStack(spacing: 25) {
                // 标题
                Text("手势与交互")
                    .font(.largeTitle)
                    .fontWeight(.bold)
                    .foregroundColor(.blue)
                
                // 1. 点击手势
                VStack {
                    Text("1. 点击手势")
                        .font(.headline)
                    Rectangle()
                        .fill(isTapped ? Color.green : Color.blue)
                        .frame(width: 200, height: 80)
                        .cornerRadius(10)
                        .onTapGesture {
                            withAnimation {
                                isTapped.toggle()
                            }
                        }
                    Text("状态: \(isTapped ? "已点击" : "未点击")")
                }
                .padding()
                .background(Color.gray.opacity(0.1))
                .cornerRadius(10)
                
                // 2. 长按手势
                VStack {
                    Text("2. 长按手势")
                        .font(.headline)
                    Rectangle()
                        .fill(isLongPressed ? Color.red : Color.green)
                        .frame(width: 200, height: 80)
                        .cornerRadius(10)
                        .onLongPressGesture {
                            withAnimation {
                                isLongPressed.toggle()
                            }
                        }
                    Text("状态: \(isLongPressed ? "长按中" : "未长按")")
                }
                .padding()
                .background(Color.gray.opacity(0.1))
                .cornerRadius(10)
                
                // 3. 拖拽手势
                VStack {
                    Text("3. 拖拽手势")
                        .font(.headline)
                    Circle()
                        .fill(.red)
                        .frame(width: 60, height: 60)
                        .offset(dragOffset)
                        .gesture(
                            DragGesture()
                                .onChanged { value in
                                    dragOffset = value.translation
                                }
                                .onEnded { _ in
                                    withAnimation {
                                        dragOffset = .zero
                                    }
                                }
                        )
                    Text("拖拽小球后自动归位")
                        .font(.caption)
                }
                .padding()
                .background(Color.gray.opacity(0.1))
                .cornerRadius(10)
                
                // 4. 缩放手势
                VStack {
                    Text("4. 缩放手势")
                        .font(.headline)
                    Rectangle()
                        .fill(.purple)
                        .frame(width: 120, height: 120)
                        .cornerRadius(10)
                        .scaleEffect(scale)
                        .gesture(
                            MagnificationGesture()
                                .onChanged { value in
                                    scale = value
                                }
                                .onEnded { _ in
                                    withAnimation {
                                        scale = 1.0
                                    }
                                }
                        )
                    Text("双指缩放,松手复位")
                        .font(.caption)
                }
                .padding()
                .background(Color.gray.opacity(0.1))
                .cornerRadius(10)
                
                // 5. 旋转手势
                VStack {
                    Text("5. 旋转手势")
                        .font(.headline)
                    Rectangle()
                        .fill(.orange)
                        .frame(width: 120, height: 120)
                        .cornerRadius(10)
                        .rotationEffect(.degrees(rotation))
                        .gesture(
                            RotationGesture()
                                .onChanged { value in
                                    rotation = value.degrees
                                }
                                .onEnded { _ in
                                    withAnimation {
                                        rotation = 0
                                    }
                                }
                        )
                    Text("双指旋转,松手复位")
                        .font(.caption)
                }
                .padding()
                .background(Color.gray.opacity(0.1))
                .cornerRadius(10)
                
                // 6. 组合手势
                VStack {
                    Text("6. 组合手势")
                        .font(.headline)
                    Rectangle()
                        .fill(.blue)
                        .frame(width: 120, height: 120)
                        .cornerRadius(10)
                        .offset(offset)
                        .scaleEffect(scale)
                        .rotationEffect(.degrees(rotation))
                        .gesture(
                            DragGesture()
                                .onChanged { value in
                                    offset = value.translation
                                }
                                .onEnded { _ in
                                    withAnimation {
                                        offset = .zero
                                    }
                                }
                        )
                        .simultaneousGesture(
                            MagnificationGesture()
                                .onChanged { value in
                                    scale = value
                                }
                                .onEnded { _ in
                                    withAnimation {
                                        scale = 1.0
                                    }
                                }
                        )
                        .simultaneousGesture(
                            RotationGesture()
                                .onChanged { value in
                                    rotation = value.degrees
                                }
                                .onEnded { _ in
                                    withAnimation {
                                        rotation = 0
                                    }
                                }
                        )
                    Text("支持拖动、缩放、旋转")
                        .font(.caption)
                }
                .padding()
                .background(Color.gray.opacity(0.1))
                .cornerRadius(10)
                
                // 7. 双击计数
                VStack {
                    Text("7. 双击计数")
                        .font(.headline)
                    Rectangle()
                        .fill(.teal)
                        .frame(width: 200, height: 80)
                        .cornerRadius(10)
                        .onTapGesture(count: 2) {
                            tapCount += 1
                        }
                    Text("双击次数: \(tapCount)")
                    Button("重置") {
                        tapCount = 0
                    }
                    .buttonStyle(.bordered)
                }
                .padding()
                .background(Color.gray.opacity(0.1))
                .cornerRadius(10)
            }
            .padding()
        }
    }
}

#Preview {
    GestureAndInteractionDemo()
}

常见问题与解决方案

1. 手势不响应

问题:视图添加了手势,但没有响应。

解决方案

  • 确保视图有足够的大小(例如,不要将手势添加到 frame(width: 0, height: 0) 的视图上)
  • 检查视图是否被其他视图遮挡(使用 .contentShape(Rectangle()) 扩大可点击区域)
  • 确保没有其他手势冲突
// 扩大点击区域
Rectangle()
    .fill(.clear)
    .contentShape(Rectangle())  // 使透明区域也能响应手势
    .onTapGesture { }

2. 组合手势冲突

问题:多个手势同时应用时出现冲突。

解决方案

  • 使用 .simultaneousGesture() 修饰符来允许同时处理多个手势
  • 使用 .highPriorityGesture() 让某个手势优先
  • 使用 .gesture()including 参数控制手势识别行为
// 高优先级手势(会阻断其他手势)
view.highPriorityGesture(
    TapGesture().onEnded { }
)

// 同时识别多个手势
view.simultaneousGesture(dragGesture)
    .simultaneousGesture(rotationGesture)

3. 手势状态管理

问题:手势结束后状态没有正确更新。

解决方案

  • .onEnded 回调中正确更新状态
  • 对于需要持久化的状态,使用 @State@StateObject
DragGesture()
    .onChanged { value in
        // 实时更新
        offset = value.translation
    }
    .onEnded { value in
        // 结束时的处理
        withAnimation {
            offset = .zero
        }
    }

总结

本章介绍了 SwiftUI 中的手势与交互,包括:

  • 基本手势类型:点击、长按、拖拽、缩放、旋转
  • 手势的状态管理和数据处理
  • 组合手势的实现方法(.simultaneousGesture
  • 手势的高级应用技巧(优先级控制、自定义识别)

通过这些手势,可以使应用界面更加交互友好,提升用户体验。在实际开发中,合理使用手势可以为应用增添交互性,使界面操作更加直观自然。


参考资料


本内容为《SwiftUI 进阶》第三章,欢迎关注后续更新。