添加手势行为最简单的办法,就是用内置的手势事件。简单说:内置手势就是手势结构体前面带 on
字符的。
Text("Tap me!")
.onTapGesture { print("Tapped!") }
可以设置 count
参数,处理双击、和三次点击
.onTapGesture(count: 2, perform: { self.isOn.toggle() })
.onTapGesture(count: 3) { print("Double tapped!") }
可以根据闭包中提供的 location 参数,获取具体视图内点击的位置(相对位置坐标)
Circle()
.fill(.red)
.frame(width: 100, height: 100)
.onTapGesture { location in
print("Tapped at \\(location)")
}
如果想要获取全局位置(即相对于整个屏幕左上角的点击位置),应该添加 coordinateSpace 参数,如下所示:
Circle()
.fill(.red)
.frame(width: 100, height: 100)
.onTapGesture(coordinateSpace: .global) { location in
print("Tapped at \\(location)")
}
.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 recognizer 】。这些手势识别器包含一些具体事件闭包,这些闭包将在事件激活时运行。常用的手势识别器有:DragGesture
、LongPressGesture
、MagnifyGesture
、RotateGesture
等
// 首先添加 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
结合使用时,可以用到两个事件修饰符:
.onChanged()
修饰符:代表在手势状态发生变化时运行闭包.onEnded()
修饰符:代表在手势识别结束时运行闭包高级的需求,需要用手势修饰符结合 @GestureState
手势状态属性,才能更方便地追踪手势的状态变化。使用 @GestureState
手势状态属性后,手势可以多支持一个 .updating
事件修饰符。
.updating
修饰符:代表可以监测手势从开始到结束的整个过程。其使用方法如下:
updating($GestureState, body: { (value, state, transaction) in ... })
$GestureState
:是指该函数要绑定手势状态属性 @GestureState
value
参数:代表手势当前状态,这个值会根据不同手势而有所不同,它对声明的 @GestureState
类型有影响,例如布尔值或其他state
参数:是一个 in-out 参数,可以让你更新手势状态属性的值transaction
参数:代表手势上下文,储存了目前状态处理更新的内容,例如动画过渡效果// 例子:
@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>
@GestureState
状态属性支持自动复位。这是它的优势,即当手势结束后,它会自动设定手势状态的值为它的初始值@State
状态属性,搭配的是用 onChange
、onEnded
事件修饰符,它只能代表状态是否发生变化,大多是一次性触发@GestureState
状态属性,搭配的是 updating
事件修饰符,它代表状态持续发生变化,大多是持续触发<aside> 💡 所以如果是只需要监听某一刻的状态改变,很多时候用 @State 搭配 onChange 就够用了,但如果要监测手势的整个过程中的变化,例如拖拽手势,随着拖拽的距离变化,视图也要发生变化,那就要用 @GestureState 搭配 updating 方法了。
</aside>
任何 SwiftUI 视图都可以附加手势识别器,而这些手势识别器又可以附加闭包,这些闭包将在识别器激活时运行。
@State private var isPressed = false
VStack{...}
.gesture(
TapGesture(count: 1)
.onEnded({ self.isPressed.toggle() })
)
可设置参数:
@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()
}
)
可设置参数:
事件修饰符:
@GestureState
搭配使用时,其中的 value
参数是布尔值类型,当它为 true
时,代表侦测到点击事件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
}
}
)
可设置参数:
DragGesture(minimumDistance: 300)
事件修饰符:
onChanged:闭包被赋予一个参数,该参数描述了拖动操作的相关信息(开始位置、当前位置、移动距离等)
onEnded:闭包被赋予一个参数,该参数描述了拖动操作的相关信息(开始位置、当前位置、移动距离等)
// 例如我们可以在拖拽松手后,结束事件触发时,根据拖拽距离做判断
.onEnded({ value in
if value.translation.height > 100 {
//如果拖拽的最终状态大于100,就让它变到200
self.currentMenuY = 200
}
else {
//否则,如果拖拽的最终状态不够100,就让它回到0
self.currentMenuY = 0
}
})
updating:当与 @GestureState
搭配使用时:
value
参数:对拖拽来说,储存的是包括位移在内的拖拽行为的相关数据,如拖动的位移 value.translation
state
参数:即绑定的 dragOffset
属性,设定 state 等于 value.translation
就是 dragOffset = value.translation
。这意味着手势状态属性 dragOffset
将根据用户拖动手势来实时更新,再将它应用到别的修饰符,即可实现移动效果transaction
是手势上下文,是一个事务对象,用于处理手势状态的过渡效果,例如动画// 首先要声明一个 GestureState 状态参数
@GestureState var dragOffset = CGSize.zero
Image(systemName: "star.circle.fill")
.offset(x: dragOffset.width, y: dragOffset.height)
.animation(.easeInOut)
.gesture(
DragGesture()
// 调用 updating 方法监听更新,监听对象是绑定前面的手势状态属性,该方法最后的闭包接收3个参数
.updating($dragOffset, body: { (value, state, transaction) in
state = value.translation
})
)
为什么拖拽后,图片会回到它起始位置?因为使用 @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
用于跟踪视图的捏合缩放,它可以绑定到 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
}
)
}
}
@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
}
)
}