常见手势用例

1. 判断成立再启用手势

.gesture(
		// 修饰器内部可以加判断,符合条件时手势才生效
		show ?
		// 判断成立时,执行这个
		DragGesture()
	     .onChanged{ value in
         self.activeView = value.translation
       }
		   .onEnded{ value in
		     self.activeView = CGSize.zero
	     }
    // 判断不成立时执行这个。 用 nil 代表不符合判断,就不返回任何手势
		: nil
)

// 这其实是一个三元表达式

2. 结合使用 guard 语句

.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
				}
)

3. 使用 Enum 区分复合手势状态

见 snippetsLab 代码收集。

4. 建立通用的可拖拽视图

见 snippetsLab 代码收集。


手势冲突处理

当您有两个或多个可能同时识别的手势时,可能会产生冲突。

1. 默认优先响应子元素

这种情况下,SwiftUI 将始终优先响应子元素的手势。这意味着当您点击上面的文本视图时,将看到“Text tapped”。

//例如:将一个手势附加到视图,并将相同的手势附加到其父级
struct ContentView: View {
    var body: some View {
        VStack {
            Text("Hello, World!")
                .onTapGesture {
                    print("Text tapped")
                }
        }
        .onTapGesture {
            print("VStack tapped")
        }
    }
}

2. highPriorityGesture 调整优先级

如果一个 SwiftUI 视图位于另一个视图中,并且两者具有相同的手势识别器,则系统将始终在父级之前触发子级的识别器。您可以使用 highPriorityGesture() 更改此行为,这将强制系统优先考虑一个手势而不是另一个手势。

 var body: some View {
        VStack {
            Text("Hello, World!")
                .onTapGesture {
                    print("Text tapped")
                }
        }
        // 代替了之前的 .gesture
        .highPriorityGesture(
            TapGesture()
                .onEnded {
                    print("VStack tapped")
                }
        )
}

3. defersSystemGestures 防止触发系统手势

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 上,它背后做了三件事情:

  1. 如果用户从顶部向下拉,他们会看到一个需要再次拉动的小选项卡,而不是立即出现控制中心。用户更难意外激活控制中心
  2. 主屏幕指示器将淡出至较低的不透明度,如果用户直接拖动该淡出的主屏幕指示器,则其将淡入。然后,他们可以再次向上滑动以进入任务切换器或主屏幕。
  3. 如果用户从底部向上滑动到 home indicator 的任一侧,则会触发我们的拖动手势

复合手势

在某些情況下,同一个视图可能会用到多种手势辨识器。举例,你想要使用者在开始拖拽之前先按著图片不放,我们便需要结合长按与拖拽手势。SwiftUI 可以很容易地结合不同手势来执行复杂的互动。它提供3种复合手势形态,包括:

1. 依序 sequenced

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> 💡 当在复合手势中使用 updatingonEnded 方法时,其 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)
    }

2. 同时 simultaneousGesture

默认情况下,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")
                }
        )
    }
}

3. 专有 exclusively

专有形态 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!"
                        }
                    }
            )
    }
}

<aside> 💡

如果是结合长按手势 和 拖拽手势,然后使用 updating 方法,那也和 onEnded 方法类似。方法中的 value 参数也是包含两个手势的(也就是长按和拖拽)。所以要用 switch 语句来区分手势。你可以使用 .first.second 找出要处理的手势。

</aside>

以上例子,如果不使用 exclusively(before:) 方法来写,而是通过多个 .gesture() 修饰符将手势应用在同一视图上,其表现会有所不同,尤其在手势之间可能发生冲突的情况下。假设改用使用两个独立的 .gesture() 修饰符来处理 TapGestureLongPressGesture

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!"
                    }
            )
    }
}

两者关键差异:

<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)
}