.gesture(
// 修饰器内部可以加判断,符合条件时手势才生效
show ?
// 判断成立时,执行这个
DragGesture()
.onChanged{ value in
self.activeView = value.translation
}
.onEnded{ value in
self.activeView = CGSize.zero
}
// 判断不成立时执行这个。 用 nil 代表不符合判断,就不返回任何手势
: nil
)
// 这其实是一个三元表达式
.gesture(
DragGesture()
.onChanged{ value in
// 保证拖动距离小于300,否则就中断了
guard value.translation.height < 300 else{
return
}
// 保证拖动距离必须大于0,否则就中断了
guard value.translation.height > 0 else{
return
}
self.activeView = value.translation
}
)
见 snippetsLab 代码收集。
见 snippetsLab 代码收集。
当您有两个或多个可能同时识别的手势时,可能会产生冲突。
这种情况下,SwiftUI 将始终优先响应子元素的手势。这意味着当您点击上面的文本视图时,将看到“Text tapped”。
//例如:将一个手势附加到视图,并将相同的手势附加到其父级
struct ContentView: View {
var body: some View {
VStack {
Text("Hello, World!")
.onTapGesture {
print("Text tapped")
}
}
.onTapGesture {
print("VStack tapped")
}
}
}
如果一个 SwiftUI 视图位于另一个视图中,并且两者具有相同的手势识别器,则系统将始终在父级之前触发子级的识别器。您可以使用 highPriorityGesture()
更改此行为,这将强制系统优先考虑一个手势而不是另一个手势。
var body: some View {
VStack {
Text("Hello, World!")
.onTapGesture {
print("Text tapped")
}
}
// 代替了之前的 .gesture
.highPriorityGesture(
TapGesture()
.onEnded {
print("VStack tapped")
}
)
}
SwiftUI 的 defersSystemGestures()
修饰符允许我们自己的手势优先于系统内置手势。这在很多地方都很重要,例如用户可能会频繁滑动的游戏。举个例子,您可能会使用拖动手势来让用户控制某些输入的值,也许他们正在对颜色进行精细控制,也许他们正在处理诸如特雷门琴之类的音频… 这里就可以使用 defersSystemGestures()
:
struct ContentView: View {
@State private var input = 0.0
var body: some View {
Text("Current value: \\(input)")
.frame(maxWidth: .infinity, maxHeight: .infinity)
.contentShape(Rectangle())
.gesture(
DragGesture()
.onChanged { value in
input = value.location.y - value.startLocation.y
}
)
.defersSystemGestures(on: .vertical)
}
}
在 iOS 上,它背后做了三件事情:
在某些情況下,同一个视图可能会用到多种手势辨识器。举例,你想要使用者在开始拖拽之前先按著图片不放,我们便需要结合长按与拖拽手势。SwiftUI 可以很容易地结合不同手势来执行复杂的互动。它提供3种复合手势形态,包括:
SwiftUI 允许我们创建手势序列,其中一个手势只有在另一个手势首先成功时才会激活。这使得仅在两个手势连续出现时才会触发操作。例如,用户长按视图,然后才可以拖动视图。这就不能简单将不同手势附加到视图上,而要用“依序复合手势类型”来结合不同手势,SwiftUI 会以特定的顺序来识别手势。示例如下:先识别长按,长按成功后才到识别拖拽…
// 写法一:
@GestureState private var dragState = DragState.inactive
@State private var position = CGSize.zero
enum DragState {
case inactive
case pressing(index: Int? = nil)
case dragging(index: Int? = nil, translation: CGSize)
var index : Int? {
switch self {
case .pressing(let index), .dragging(let index, _):
return index
case .inactive:
return nil
}
}
var translation : CGSize? {
switch self {
case .dragging(_, let translation):
return translation
case .inactive, .pressing:
return .zero
}
}
}
.gesture(
// 1.长按手势
LongPressGesture(minimumDuration: 1.0)
.updating($isPressed){ (currentState, state, transaction) in
state = currentState
}
// 2.拖拽手势
// sequenced 关键词可以連結長按與拖曳手勢在一起。这里告訴,LongPressGesture 應該在 DragGesture 之前發生
.sequenced(before: DragGesture())
// value 参数实际上包含了两个手势(长按与拖拽),这里可以用switch区分,可以使用.first 和 .second来找出要处理的手势
// .first是长按; .second是拖拽,用let drag取出拖拽的数据
.updating($dragState, body: { (value, state, transaction) in
switch value {
// 括号中的 true 表示 LongPressGesture 已经被触发
case .first(true):
state = .pressing
print("Tapping")
// 括号中的 true 表示 DragGesture 已经被触发。也就是说,用户在长按之后开始拖动
// let drag 是将拖动手势的值赋给局部变量 drag。这个值包含了拖动手势的信息,如位置和偏移量
case .second(true, let drag):
state = .dragging(translation: drag?.translation ?? .zero)
default:
break
}
})
.onEnded({ (value) in
guard case .second(true, let drag?) = value else {
return
}
self.position.height += drag.translation.height
self.position.width += drag.translation.width
})
)
<aside>
💡 当在复合手势中使用 updating
和 onEnded
方法时,其 value
参数实际上是包含了两个手势的,所以这里用 switch
进行区分。你可以使用 .first
(代表组合手势中的第1个手势) 和 .second
(代表组合手势中的第2个手势)的 case 来找出要处理的手势。
</aside>
//写法二:
// 状态属性:记录圆圈被拖动了多远
@State private var offset = CGSize.zero
// 状态属性:记录圆圈是否正处于拖动的过程中
@State private var isDragging = false
var body: some View {
// 拖拽手势,更新 offset 的值,以及 isDragging 的值
let dragGesture = DragGesture()
.onChanged { value in
offset = value.translation
}
.onEnded { _ in
withAnimation {
offset = .zero
isDragging = false
}
}
// 长按之后启用 isDragging
let pressGesture = LongPressGesture()
.onEnded { value in
withAnimation {
isDragging = true
}
}
// 创建一个手势序列,强制必须先长按,才可以进行拖拽
// a combined gesture that forces the user to long press then drag
let combined = pressGesture.sequenced(before: dragGesture)
// a 64x64 circle that scales up when it's dragged, sets its offset to whatever we had back from the drag gesture, and uses our combined gesture
Circle()
.fill(.red)
.frame(width: 64, height: 64)
.scaleEffect(isDragging ? 1.5 : 1)
.offset(offset)
.gesture(combined)
}
默认情况下,SwiftUI 一次只会触发一个手势识别器操作,通常是视图层次结构中最前面的视图。例如,它更喜欢在子视图而不是其父视图上使用识别器。如果您想同时触发不同视图上的两个手势,应该使用 simultaneousGesture()
修饰符创建第二个手势,告诉 SwiftUI 希望父手势和子手势同时触发。如下所示:
struct ContentView: View {
var body: some View {
VStack {
Circle()
.fill(.red)
.frame(width: 200, height: 200)
.onTapGesture {
print("Circle tapped")
}
}
.simultaneousGesture(
TapGesture()
.onEnded { _ in
print("VStack tapped")
}
)
}
}
专有形态 exclusively
是一个可用于处理手势冲突的方式,它可以识别你指定的多个手势。不过当一个手势被侦测到时,另一个手势就会被忽略。换句话说,exclusively
的使用方式是将两个手势组合起来,当第一个手势成功识别并触发时,第二个手势将不会被检测到。如果第一个手势没有触发,则第二个手势可以起作用。
exclusively
特别适合在你想要确保多个手势不会同时触发时使用。例如:
// 下面是简单的例子,展示如何在 SwiftUI 中使用 exclusively 来组合两个手势(TapGesture 和 LongPressGesture)
// 当用户进行单击时,只有 TapGesture 被触发;如果用户按住不放,只有 LongPressGesture 被触发。
struct ContentView: View {
@State private var message = "Tap or Long Press"
var body: some View {
Text(message)
.onTapGesture {
message = "Tapped!"
}
.gesture(
TapGesture()
.exclusively(before: LongPressGesture())
.onEnded { value in
switch value {
case .first:
message = "Tapped!"
case .second:
message = "Long Pressed!"
}
}
)
}
}
exclusively(before:)
方法将两个手势组合。TapGesture
手势具有较高优先级。如果识别到点击手势,则长按手势不会触发。如果点击手势未被识别(例如用户长按了视图),则长按手势将会生效。onEnded
来捕捉手势的结束状态,并通过 value
参数来判断哪个手势被识别到了。value
可以是 .first
或 .second
,分别代表第一个或第二个手势。<aside> 💡
如果是结合长按手势 和 拖拽手势,然后使用 updating
方法,那也和 onEnded
方法类似。方法中的 value
参数也是包含两个手势的(也就是长按和拖拽)。所以要用 switch
语句来区分手势。你可以使用 .first
與 .second
找出要处理的手势。
</aside>
以上例子,如果不使用 exclusively(before:)
方法来写,而是通过多个 .gesture()
修饰符将手势应用在同一视图上,其表现会有所不同,尤其在手势之间可能发生冲突的情况下。假设改用使用两个独立的 .gesture()
修饰符来处理 TapGesture
和 LongPressGesture
:
struct ContentView: View {
@State private var message = "Tap or Long Press"
var body: some View {
Text(message)
.gesture(
TapGesture()
.onEnded {
message = "Tapped!"
}
)
.gesture(
LongPressGesture()
.onEnded { _ in
message = "Long Pressed!"
}
)
}
}
两者关键差异:
.exclusively(before:)
:可以显式地定义手势的优先级。当两个手势之间有冲突时,只会触发优先的手势(即 before:
指定的手势)。例子中,如果用户快速点击,TapGesture
会被触发;如果用户长按,则 LongPressGesture
会触发,而点击手势不会再响应。.gesture(TapGesture())
和 .gesture(LongPressGesture())
:则手势的优先级则是隐式的,取决于 SwiftUI 的手势处理机制。默认情况下,SwiftUI 会尝试同时处理多个手势,因此可能会导致手势同时触发或干扰彼此。例如,如果你快速点击然后立即长按,可能会触发两个手势的回调,具体行为取决于 SwiftUI 如何决定处理这些手势。你可能会先看到 "Tapped!"
消息,然后又看到 "Long Pressed!"
<aside> 💡
使用 .exclusively(before:)
时,手势之间有明确的排他性,只有其中一个手势会被触发。使用多个 .gesture()
修饰符时,手势可能会同时触发,具体行为依赖于 SwiftUI 的处理机制,在手势冲突的情况下可能导致意外因此,如果你希望明确地只触发一个手势而不是同时触发,建议使用 exclusively
这样的机制来显式管理优先级。
</aside>
例如:定义一个 some Gesture
类型的手势,可以被后面 .gesture
引用
// GestureState 属性代表一个手势事件在整个期間是否被偵測到。
@GestureState private var isDetectingLongPress = false
// 这里另外用了一个状态参数
@State private var completedLongPress = false
var longPress: some Gesture {
//设置最短持续时间3秒
LongPressGesture(minimumDuration: 3)
//使用 Updating 方法:在长按手势执行期间这个方法都会被调用,当手势值发生变化时,SwiftUI调用的回调包括以下3个参数:
//value(currentState) 参数代表手势目前更新状态
//state(gestureState) 参数是之前 isDetectingLongPress 属性的值
//设定 “gestureState = currentState”,代表 isDetectingLongPress 属性会持续更新为长按手势的最新状态
//transaction 参数是手势上下文,储存了目前状态处理更新的內容,例如动画
.updating($isDetectingLongPress) {
currentState, gestureState, transaction in
gestureState = currentState
//设置transaction的动画内容
transaction.animation = Animation.easeIn(duration: 2.0)
}
//设置长按手势识别结束时,更新状态参数。(finished 是 onEnded的参数,表示手势识别的最后状态值)
.onEnded {
finished in
self.completedLongPress = finished
}
}
//给相应的视图加上手势识别:
var body: some View {
Circle()
//如果isDetectingLongPress=true,则显示红色,否则再判断 completedLongPress是否为真,如果为真显示绿色,否则显示蓝色
.fill(self.isDetectingLongPress ? Color.red : (self.completedLongPress ? Color.green : Color.blue))
.frame(width: 100, height: 100, alignment: .center)
.gesture(longPress)
}