简单:使用内置手势

添加手势行为最简单的办法,就是用内置的手势事件。简单说:内置手势就是手势结构体前面带 on 字符的。

点击 onTapGesture

Text("Tap me!")
.onTapGesture { print("Tapped!") }

长按 onLongPressGesture

.onLongPressGesture{ self.press.toggle() }

// 可以自定义按下的最短持续时间,例如,这只会在两秒后触发
.onLongPressGesture(minimumDuration: 2) { print("Long pressed!") }

// 可以添加第二个闭包,该闭包会在手势状态发生更改时触发。这将被赋予一个【布尔值参数】作为输入,像这样:
.onLongPressGesture(minimumDuration: 2) {
    print("Long pressed!")
} onPressingChanged: { inProgress in
    print("In progress: \\(inProgress)!")
}

// 1. 一旦按下,就会调用更改闭包,并将其参数设置为 true (inProgress = true)
// 2. 如果在识别手势之前释放(例如2秒识别器在1秒后释放),则将调用第二个更改闭包,并将参数设置为 false(inProgress = false)
// 3. 如果按完识别器的整个长度,则将调用更改闭包,并将参数设置为 false(因为手势不再处于激活状态)(inProgress = false),并且第一个完成闭包,将被调用;

进阶:.gesture 修饰符

对于高级手势应该用【.gesture 修饰符】 结合 【手势识别器 gesture recognizer 】。这些手势识别器包含一些具体事件闭包,这些闭包将在事件激活时运行。常用的手势识别器有:DragGestureLongPressGestureMagnifyGestureRotateGesture

声明方法

// 首先添加 gesture 修饰符
.gesture(
		// 然后使用具体手势识别器
		DragGesture()
				// 手势识别器有对应的事件修饰符,在里面添加闭包
			  .onChanged{ value in
			    ...
			  }
		    .onEnded{ value in
		      ...
		    }
	)
// 也可以先声明一个手势类型的结构
var tapGesture: some Gesture {
    TapGesture()
        .onEnded {
            withAnimation {
                color = Color.random()
            }
        }
}

// 后续再作为入参添加到 .gesture 修饰符里使用
Rectangle()
		.foregroundColor(color)
		.frame(width: 250, height: 450)
		.gesture(tapGesture)

与 @State 结合

一般的需求,用手势修饰符结合 @State 状态参数,就可以实现。当和 @State 结合使用时,可以用到两个事件修饰符:

与 @GestureState 结合

高级的需求,需要用手势修饰符结合 @GestureState 手势状态属性,才能更方便地追踪手势的状态变化。使用 @GestureState 手势状态属性后,手势可以多支持一个 .updating 事件修饰符。

.updating 修饰符:代表可以监测手势从开始到结束的整个过程。其使用方法如下:

// 例子:
@GestureState private var longPressTap = false
@State private var completedLongPress = false

Image(systemName: "star.circle.fill")
    .opacity(longPressTap ? 0.4 : 1.0)
    .scaleEffect(longPressTap ? 0.5 : 1.0)
    .gesture(
	    LongPressGesture(minimumDuration: 3)
			// 使用 Updating 方法:在长按手势执行期间这个方法都会被调用,当手势值发生变化时,闭包中包括以下3个参数: 
	    .updating($longPressTap) { currentState, gestureState, transaction in
	    
		    // 第一个参数 currentState 代表当前手势状态。对于 LongPressGesture,手势状态是一个 Bool 值,表示长按是否正在进行中
		    // 如果 currentState 为 true,表示长按正在进行,如果为 false,表示长按已经结束

				// 第二个参数 gestureState 代表手势状态的绑定变量,即前面 longPressTap 属性的值
				// 设定 “gestureState = currentState”,代表 longPressTap 属性会持续更新为长按手势的最新状态
				// longPressTap 属性的更新,就会影响前面的 透明度 和 缩放效果 修饰符
        gestureState = currentState
        
        // 第三个参数 transaction 代表手势动画的结构体,它允许你指定手势动画的类型、持续时间和其他参数。
				// 这里 transaction 被用来设置一个持续时间为 2 秒的 easeIn 动画
        transaction.animation = Animation.easeIn(duration: 2.0)
        
	    }
			// 设置长按手势识别结束时,更新状态参数。( finished 是 onEnded 闭包的参数,表示手势识别的最后状态值)
      .onEnded { finished in
        self.completedLongPress = finished
      }

    )

手势状态属性 @GestureState 声明的类型与具体使用的手势有关。不同的手势会提供不同的状态信息,因此 @GestureState 需要与之匹配。以下列出常见手势及其对应的 @GestureState 类型:

手势 类型
TapGesture @GestureState private var isTapped: Bool = false
LongPressGesture @GestureState private var isDetectingLongPress: Bool = false
DragGesture @GestureState private var dragState: DragGesture.Value = .zero
MagnificationGesture @GestureState private var scale: CGFloat = 0.0
RotationGesture @GestureState private var rotation: Angle = .zero
自定义手势(使用 DragGesture 示例) @GestureState private var dragInfo: (startLocation: CGPoint, currentLocation: CGPoint, translation: CGSize) = (.zero, .zero, .zero)

<aside> 💡

需要注意,这些只是常见的用法。根据具体的需求,你可能会需要调整状态的类型或结构。例如你也可以声明一个枚举,只是在其关联值里面用到手势提供的状态数据类型;或者对于更复杂的手势交互,你可能会使用自定义的结构体来存储更多的状态信息。

</aside>

两种属性的区别

<aside> 💡 所以如果是只需要监听某一刻的状态改变,很多时候用 @State 搭配 onChange 就够用了,但如果要监测手势的整个过程中的变化,例如拖拽手势,随着拖拽的距离变化,视图也要发生变化,那就要用 @GestureState 搭配 updating 方法了。

</aside>


常用手势识别器

任何 SwiftUI 视图都可以附加手势识别器,而这些手势识别器又可以附加闭包,这些闭包将在识别器激活时运行。

点击 TapGesture

@State private var isPressed = false
VStack{...}
.gesture(
		TapGesture(count: 1)
			.onEnded({ self.isPressed.toggle() })
)

可设置参数:


长按 LongPressGesture

@State var tap = false
@State var press = false

.gesture(
		LongPressGesture(minimumDuration: 2, maximumDistance: 10)
		// 长按手势刚点击下去的时候,其值会发生变化,onChanged会触发
		.onChanged{ value in 
			self.tap = true 
		}
		// 长按手势达到设置的最小持续时间的时候,长按结束,onEnd会触发
	  .onEnded{ value in 
		  self.press.toggle() 
		}
)

可设置参数:

事件修饰符:


拖拽 DragGesture

struct ContentView: View {

    @State private var dragOffset = CGSize.zero
    
    var body: some View {
        VStack {
            Image("rome")
                .offset(dragOffset)
                .gesture(
                    DragGesture(minimumDistance: 50)
		                    // 读取闭包参数的 translation 属性,它告诉我们它从起点移动了多远
                        .onChanged { gesture in
                            dragOffset = gesture.translation
                        }
                        // 忽略了闭包参数,直接将 dragAmount 设置回零
                        .onEnded { gesture in
                            dragOffset = .zero
                        }
                )
        }
    }
}

// 添加隐性动画:可以使回弹更平滑
.animation(.default, value: dragOffset)

// 添加显性动画:可以使回弹更平滑
.gesture(
	  DragGesture()
	    .onChanged { 
			    dragOffset = $0.translation 
			}
      .onEnded { _ in 
					withAnimation(.bouncy) {
			        dragOffset = .zero
			    }
			}
)

可设置参数:

事件修饰符:

为什么拖拽后,图片会回到它起始位置?因为使用 @GestureState 的好处是,当手势动作结束时,它会重设属性值至原来的值。因此,当你手指放开拖拽之后,dragOffset 会重新设置为 CGSize.zero ,也就是回到原來的位置。

// 如果不想回到原来位置,需要用另外一个属性来储存最终的位置
@State private var position = CGSize.zero

// 然后在DragGesture里面加上结束代码,在拖拽结束的时候会调用
.onEnded({ 
		(value) in
		self.position.height += value.translation.height
		self.position.width += value.translation.width
})

// 并且更新offset代码,确保每次拖拽都加上之前的偏移量
.offset(x: position.width + dragOffset.width, y: position.height + dragOffset.height)

缩放 MagnifyGesture

MagnifyGesture 用于跟踪视图的捏合缩放,它可以绑定到 scaleEffect() 修饰符,以便用户的捏合手势自动放大或缩小视图。如果您想在他们完成手势后保持视图的缩放级别,您应该同时跟踪视图的当前缩放级别和总缩放级别,如下所示:

struct ContentView: View {

    @State private var currentZoom = 0.0
    @State private var totalZoom = 1.0

    var body: some View {
        Image("singapore")
            .scaleEffect(currentZoom + totalZoom)
            .gesture(
                MagnifyGesture()
                    .onChanged { value in
                        currentZoom = value.magnification - 1
                    }
                    .onEnded { value in
                        totalZoom += currentZoom
                        currentZoom = 0
                    }
            )
            // 从 value.magnification 中减去 1 很重要,因为 1 是新手势的默认值
            // 使用 accessibilityZoomAction() 修饰符允许辅助技术控制缩放级别
            .accessibilityZoomAction { action in
                if action.direction == .zoomIn {
                    totalZoom += 1
                } else {
                    totalZoom -= 1
                }
            }
    }
}

如果您想跟踪他们的手势但每次都重置回 0,请使用 @GestureState

struct ContentView: View {
    @GestureState private var zoom = 1.0
    var body: some View {
        Image("singapore")
            .scaleEffect(zoom)
            .gesture(
                MagnifyGesture()
                    .updating($zoom) { value, gestureState, transaction in
                        gestureState = value.magnification
                    }
            )
    }
}

旋转 RotationGesture

@State private var currentAmount = Angle.zero
@State private var finalAmount = Angle.zero

var body: some View {
		Text("Hello, World!")
            .rotationEffect(currentAmount + finalAmount)
            .gesture(
                RotateGesture()
                    .onChanged { 
												value in
                        currentAmount = value.rotation
                    }
                    .onEnded { 
												value in
                        finalAmount += currentAmount
                        currentAmount = .zero
                    }
            )
}