10.手势

木木木大约 6 分钟iOSSwiftSwiftUIUI交互

10.手势

在SwiftUI中,可以通过手势识别器(Gesture Recognizers)来捕捉和响应用户的手势操作。SwiftUI提供了一些内置的手势识别器,例如拖动手势、轻击手势、长按手势等,同时也支持自定义手势识别器。

内置手势识别器

SwiftUI提供了多种内置手势识别器,可以通过视图修饰符.gesture()来添加手势识别器。常见的内置手势识别器有:

  • TapGesture:轻击手势,可以设置点击次数、手指数等属性。
  • LongPressGesture:长按手势,可以设置最短持续时间、最大移动距离等属性。
  • DragGesture:拖动手势,可以设置最大/最小拖动速度、拖动方向等属性。
  • MagnificationGesture:放大/缩小手势,可以设置最小/最大放大比例等属性。
  • RotationGesture:旋转手势,可以设置最小/最大旋转角度等属性。

TapGesture

点击手势可以响应用户的单击事件,例如单击一个按钮或图像。使用 .tapGesture() 修饰符来添加这种手势。

Text("Click me!")
    .onTapGesture {
        print("Text is tapped")
    }

在上面的代码中,当用户单击文本视图时,会在控制台上输出 "Text is tapped"。也可以将点击手势应用于任何 SwiftUI 视图,例如图片、按钮、形状等。

LongPressGesture

长按手势可以响应用户长按某个视图的事件。可以使用 .longPressGesture() 修饰符来添加此类手势。

Image(systemName: "trash")
    .foregroundColor(.red)
    .font(.largeTitle)
    .padding()
    .background(Color.white)
    .cornerRadius(40)
    .onLongPressGesture {
        print("Delete button is long pressed")
    }

在上面的代码中,使用长按手势来响应 "删除" 按钮的长按事件。当用户长按 "删除" 按钮时,将在控制台上输出 "Delete button is long pressed"。

DragGesture

Image(systemName: "arrow.clockwise")
    .font(.largeTitle)
    .padding()
    .background(Color.white)
    .cornerRadius(40)
    .rotationGesture()

在上面的代码中,使用旋转手势使箭头图标可以旋转。当用户使用两个手指旋转视图时,箭头图标将跟随手势旋转。

MagnificationGesture

缩放手势可以响应用户使用两个手指缩放视图的事件。可以使用 .scaleGesture() 修饰符来添加此类手势。

Image(systemName: "photo")
    .resizable()
    .scaledToFit()
    .padding()
    .background(Color.white)
    .cornerRadius(40)
    .scaleEffect(scale)
    .gesture(
        MagnificationGesture()
            .onChanged { value in
                self.scale = value.magnitude
            }
    )

在上面的代码中,使用缩放手势来响应用户缩放图像视图的事件。当用户使用两个手指缩放视图时,图像视图将按比例缩放。

RotationGesture

可以使用 .dragGesture() 修饰符来添加拖动手势到视图中。这个修饰符可以让用户通过拖动手势来移动视图或者执行其他操作。

struct DragView: View {
    @State private var offset = CGSize.zero

    var body: some View {
        Circle()
            .fill(Color.blue)
            .frame(width: 100, height: 100)
            .offset(offset)
            .gesture(
                DragGesture()
                    .onChanged { value in
                        self.offset = value.translation
                    }
                    .onEnded { _ in
                        self.offset = .zero
                    }
            )
    }
}

在这个例子中,我们创建了一个 Circle 视图,并使用 .offset() 修饰符将其初始位置移动到了屏幕中心。接着使用 .gesture() 修饰符将 DragGesture 添加到了这个视图中。在 DragGesture 上定义了两个回调函数,一个是 onChanged,用于处理手势拖动过程中的值变化,另一个是 onEnded,用于处理手势结束时的操作。
在 onChanged 中,更新了 offset 变量,将其设置为手势的 translation 值,也就是手指相对于视图初始位置的移动距离。在 onEnded 中,将 offset 重置为 .zero,以将视图移回到初始位置。

自定义手势识别器

在 SwiftUI 中可以通过 Gesture 协议来创建自定义手势。 Gesture 协议是一个类型擦除协议,其具体实现取决于其遵循者。通常可以使用 Gesture 协议中的 onChanged、onEnded、onBegan 等方法来响应手势的不同状态。

通过一个例子来演示如何创建自定义手势识别器。假设我们想要创建一个手势,使得在视图上画一个 "V" 字形时,触发相应的操作。

首先,需要创建一个实现 Gesture 协议的结构体。在这个结构体中,我们需要实现 init 和 body 两个方法。

struct VGesture: Gesture {
    let minimumDistance: CGFloat = 50

    var body: some Gesture {
        DragGesture(minimumDistance: 0)
            .onChanged { value in
                let startPoint = value.startLocation
                let endPoint = value.location
                let distance = endPoint.distance(to: startPoint)
                
                guard distance >= minimumDistance else { return }
                
                let angle = endPoint.angle(to: startPoint)
                
                if angle > -45 && angle < 45 {
                    print("V gesture detected!")
                }
            }
    }

    init() { }
}

在这个例子中创建了一个名为 VGesture 的结构体,并实现了 body 方法。使用了 DragGesture,并通过 minimumDistance 属性指定了拖动手势的最小距离。在 onChanged 方法中,我们检查了手势的起点和终点之间的距离是否足够长,然后计算了这两点之间的角度,并在特定的角度范围内触发了相应的操作。
在视图中使用自定义手势识别器:

struct ContentView: View {
    var body: some View {
        Text("Hello, World!")
            .gesture(VGesture())
    }
}

将 VGesture 添加到了 Text 视图的手势中。在视图上画出一个 "V" 字形时,就会触发相应的操作。

simultaneousGesture

使用 simultaneousGesture 可以让视图同时响应多个手势,例如同时响应拖动和缩放手势:

@State private var scale: CGFloat = 1.0
@State private var offset = CGSize.zero

var body: some View {
    Image(systemName: "star.circle")
        .resizable()
        .frame(width: 200, height: 200)
        .foregroundColor(.orange)
        .scaleEffect(scale)
        .offset(offset)
        .gesture(
            MagnificationGesture()
                .onChanged { value in
                    self.scale = value.magnitude
                }
                .simultaneously(with: DragGesture()
                                    .onChanged { value in
                                        self.offset = value.translation
                                    }
                )
        )
}

使用 simultaneous(with:) 将缩放手势和拖动手势同时绑定在图像视图上。

sequenced

使用 sequenced(before:) 可以让手势按顺序执行,例如先拖动,再缩放:

@State private var scale: CGFloat = 1.0
@State private var offset = CGSize.zero

var body: some View {
    Image(systemName: "star.circle")
        .resizable()
        .frame(width: 200, height: 200)
        .foregroundColor(.orange)
        .scaleEffect(scale)
        .offset(offset)
        .gesture(
            DragGesture()
                .onChanged { value in
                    self.offset = value.translation
                }
                .sequenced(before: MagnificationGesture()
                                    .onChanged { value in
                                        self.scale = value.magnitude
                                    }
                )
        )
}

使用 sequenced(before:) 将缩放手势放在拖动手势之后执行。

exclusiveGesture

使用 exclusiveGesture 可以阻止其他手势的响应,例如只响应双指拖动手势:

@State private var offset = CGSize.zero

var body: some View {
    Image(systemName: "star.circle")
        .resizable()
        .frame(width: 200, height: 200)
        .foregroundColor(.orange)
        .offset(offset)
        .gesture(
            DragGesture(minimumDistance: 0)
                .onChanged { value in
                    self.offset = value.translation
                }
                .exclusively(before: DragGesture(minimumDistance: 50)
                                    .onChanged { value in
                                        print("Double finger drag")
                                    }
                )
        )
}

使用 exclusively(before:) 将双指拖动手势设置为独占手势,其他手势将被阻止响应。

交互动画

在 SwiftUI 中可以利用手势和动画来实现丰富的交互效果

点击缩放

可以使用 .scaleEffect 修饰符来实现点击时的缩放效果。具体实现方式是给 View 添加一个 .scaleEffect 修饰符,并在手势响应时将其设置为 0.8,释放时恢复为 1.0。

struct ScaleButton: View {
    @State private var scale: CGFloat = 1.0
    
    var body: some View {
        Text("Click Me")
            .scaleEffect(scale)
            .padding()
            .onTapGesture {
                withAnimation(.spring()) {
                    scale = 0.8
                }
                DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
                    withAnimation(.spring()) {
                        scale = 1.0
                    }
                }
            }
    }
}

在这个示例中使用了 .spring() 来定义动画类型,并在两次动画之间添加了一个延迟以确保效果正常。

拖拽移动

可以使用 .offset 修饰符来实现拖拽移动的效果。具体实现方式是给 View 添加一个 .offset 修饰符,并在手势响应时更新其值。

struct DragView: View {
    @GestureState private var dragState = DragState.inactive
    @State private var position = CGSize.zero
    
    var body: some View {
        let dragGesture = DragGesture()
            .updating($dragState) { value, dragInfo, _ in
                dragInfo = .dragging(translation: value.translation)
            }
            .onEnded { value in
                position.height += value.translation.height
                position.width += value.translation.width
            }
        
        return Circle()
            .fill(Color.blue)
            .frame(width: 100, height: 100)
            .scaleEffect(dragState.isActive ? 1.2 : 1.0)
            .offset(
                x: position.width + dragState.translation.width,
                y: position.height + dragState.translation.height
            )
            .gesture(dragGesture)
    }
}

enum DragState {
    case inactive
    case dragging(translation: CGSize)
}

使用了 @GestureState 来定义手势状态,并使用了 DragGesture 来处理拖拽事件。使用 updating 方法来更新手势状态,并在 onEnded 方法中更新位置信息。

上次编辑于:
贡献者: perhapsdone