1.2 组合 SwiftUI 手势

原文链接: https://developer.apple.com/documentation/swiftui/composing-swiftui-gestures

1.2 组合 SwiftUI 手势

组合手势以创建复杂的交互。

概述

当你为应用的视图层级添加多个手势时,需要决定这些手势如何相互作用。你可以用组合手势来定义 SwiftUI 识别手势的顺序。手势组合有三种类型:

  • 同时(Simultaneous)
  • 顺序(Sequenced)
  • 互斥(Exclusive)

当你同时组合多个手势修饰符时,SwiftUI 必须同时识别出所有子手势模式,才会认为识别到了这个组合手势。当你把一个手势修饰符接在另一个之后时,SwiftUI 必须按顺序识别出每个子手势。最后,当你以互斥方式组合手势时,只要 SwiftUI 只识别出其中一个子手势而没有识别出其他子手势,就会认为识别到了整个手势模式。

让一个手势接在另一个手势之后

当你让一个手势接在另一个手势之后时,SwiftUI 会先识别第一个手势,再识别第二个手势。例如,为了要求用户先长按才能拖拽某个视图,你可以让 DragGesture 接在 LongPressGesture 之后。

为顺序手势的状态建模

为了更容易跟踪复杂的状态,请使用一个枚举来涵盖配置视图所需的所有状态。在下面的示例中有三个重要状态:没有交互(inactive)、长按进行中(pressing)以及拖拽中(dragging)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
struct DraggableCircle: View {

    enum DragState {
        case inactive
        case pressing
        case dragging(translation: CGSize)
            
        var translation: CGSize {
            switch self {
            case .inactive, .pressing:
                return .zero
            case .dragging(let translation):
                return translation
            }
        }
            
        var isActive: Bool {
            switch self {
            case .inactive:
                return false
            case .pressing, .dragging:
                return true
            }
        }
            
        var isDragging: Bool {
            switch self {
            case .inactive, .pressing:
                return false
            case .dragging:
                return true
            }
        }
    }
        
    @GestureState private var dragState = DragState.inactive
    @State private var viewState = CGSize.zero
    ...

创建手势并更新界面状态

当你把两个手势按顺序组合时,各个回调会同时捕获两个手势的状态。在 update 回调中,新的 value 使用一个枚举来表示各种可能手势状态的组合。下面的代码把底层手势状态转换为前面定义的高层 DragState 枚举。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
    ...
    var body: some View {
        let minimumLongPressDuration = 0.5
        let longPressDrag = LongPressGesture(minimumDuration: minimumLongPressDuration)
            .sequenced(before: DragGesture())
            .updating($dragState) { value, state, transaction in
                switch value {
                // 长按开始。
                case .first(true):
                    state = .pressing
                // 长按确认,可以开始拖拽。
                case .second(true, let drag):
                    state = .dragging(translation: drag?.translation ?? .zero)
                // 拖拽结束,或长按被取消。
                default:
                    state = .inactive
                }
            }
            .onEnded { value in
                guard case .second(true, let drag?) = value else { return }
                self.viewState.width += drag.translation.width
                self.viewState.height += drag.translation.height
            }
        ...

当用户开始按住视图时,拖拽状态变为 pressing,形状下方会出现带动画的阴影。当用户完成长按、拖拽状态变为 dragging 之后,形状周围会出现一个边框,表示用户可以开始移动这个视图了。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
    ...
    Circle()
        .fill(Color.blue)
        .overlay(dragState.isDragging ? Circle().stroke(Color.white, lineWidth: 2) : nil)
        .animation(.linear(duration: minimumLongPressDuration), value: dragState.isDragging)
        .frame(width: 100, height: 100, alignment: .center)
        .offset(
            x: viewState.width + dragState.translation.width,
            y: viewState.height + dragState.translation.height
        )
        .shadow(radius: dragState.isActive ? 8 : 0)
        .animation(.linear(duration: minimumLongPressDuration), value: dragState.isActive)
        .gesture(longPressDrag)
    }
}