[SwiftUI 100 天] Flashzilla - 应用手势

485 阅读5分钟

译自 www.hackingwithswift.com/books/ios-s…

更多内容,欢迎关注公众号 「Swift花园」

喜欢文章?不如来个 🔺💛➕三连?关注专栏,关注我 🚀🚀🚀

Flashzilla:介绍

在这个项目中我们将构建一个帮助用户通过抽认卡来学习的应用 —— 比如卡片一面写着 “to buy”,另一面写着 “comprar”。当然,因为这是一个数字化的应用程序,我们不必真的担心所谓的“另一面”,只需要确保卡片在被点击时显示正确的信息。

项目的名称实际上来自我的第一个 iOS 应用 —— 我很久以前为 iPhoneOS 开发的应用,当时还没有 iPad。 Apple 在审核的时候拒绝了应用,因为产品名中包含 “Flash”,彼时 Apple 还坚持应用商店里不能出现任何 “Flash” 字眼。时光飞逝啊...

言归正传,这个项目中有许多有趣的东西值得学习,包括手势,震动反馈,定时器等。请用 Single View App 模板创建一个新的 iOS 应用,取名 Flashzilla。和往常一样,在构建实际应用之前,我们先涉及几个技术项。让我们开始吧...


译自 www.hackingwithswift.com/books/ios-s…

应用手势

SwiftUI 为视图提供了许多配合的手势,它们让我们可以专注于更重要的业务逻辑。最常见的是 onTapGesture(),此外还有另外几种手势,把它们组合在一起使用也是很有趣的。

我会跳过 onTapGesture(),因为之前已经接触过很多次了。不过,在尝试更复杂的功能之前,我们先用它的 count 参数实现双击和三击:

Text("Hello, World!")
    .onTapGesture(count: 2) {
        print("双击!")
    }

接下来是比点击复杂一点的长按,即 onLongPressGesture()

Text("Hello, World!")
    .onLongPressGesture {
        print("长按!")
    }

跟点击手势类似,长按手势也是可以定制的。例如,你可以指定按压需要满足的最短时长。动作闭包只有在按压时间达到这个给定的时长时才会触发。比如,下面这个长按动作在两秒后触发:

Text("Hello, World!")
    .onLongPressGesture(minimumDuration: 2) {
        print("长按!")
    }

你甚至可以在手势变化过程中添加另外一个闭包,闭包传入的参数是一个布尔型:

  1. 按下时该闭包即以参数为 true 被调用。
  2. 如果你在长按被识别之前松手(比如,在 2 秒的长按识别中在 1 秒就松手),那么该闭包会以参数为 false 被调用。
  3. 如果你保持按压直到识别,该闭包也会以参数为 false 被调用(因为手势识别过程已经结束),然后识别完成的闭包也被调用。

你可以自行尝试下面的代码:

Text("Hello, World!")
    .onLongPressGesture(minimumDuration: 1, pressing: { inProgress in
        print("进行中:\(inProgress)!")
    }) {
        print("长按!")
    }

对于更高级的手势,你需要使用 gesture() modifier,并提供 DragGestureLongPressGesture, MagnificationGestureRotationGestureTapGesture 中的某一个。所有这几种手势都有特定的 modifier,最常用的包括 onEnded()onChanged(),你可以用它们在手势进行中(onChanged())或者完成时(onEnded())执行动作。

举个例子,我们可以添加一个放大手势给某个视图,以便放大或者缩小视图。实现这个功能需要创建两个 @State 属性,分别存储当前的缩放增量和最后的索芳志,用于 scaleEffect() modifier,代码如下:

struct ContentView: View {
    @State private var currentAmount: CGFloat = 0
    @State private var finalAmount: CGFloat = 1

    var body: some View {
        Text("Hello, World!")
            .scaleEffect(finalAmount + currentAmount)
            .gesture(
                MagnificationGesture()
                    .onChanged { amount in
                        self.currentAmount = amount - 1
                    }
                    .onEnded { amount in
                        self.finalAmount += self.currentAmount 
                        self.currentAmount = 0
                    }
            )
    }
}

旋转视图用的是 RotationGesture,对应 rotationEffect() modifier:

struct ContentView: View {
    @State private var currentAmount: Angle = .degrees(0)
    @State private var finalAmount: Angle = .degrees(0)

    var body: some View {
        Text("Hello, World!")
            .rotationEffect(currentAmount + finalAmount)
            .gesture(
                RotationGesture()
                    .onChanged { angle in
                        self.currentAmount = angle
                    }
                    .onEnded { angle in
                        self.finalAmount += self.currentAmount
                        self.currentAmount = .degrees(0)
                    }
            )
    }
}

当手势发生冲突时,事情就变得有趣了 —— 你有两个或者两个以上的手势需要同时识别,比如一个给当前视图,一个给父视图。

例如,下面的代码对某个文本视图及其父视图都添加了 onTapGesture()

struct ContentView: View {
    var body: some View {
        VStack {
            Text("Hello, World!")
                .onTapGesture {
                    print("文本被点击")
                }
        }
        .onTapGesture {
            print("VStack被点击")
        }
    }
}

在上面的代码中 SwiftUI 会始终给子视图赋予更高的优先级,也就是说,当你点击时,你会看到打印的消息是 “文本被点击”。不过,假如你想要改变这个优先级,可以使用 highPriorityGesture() modifier 来强制父视图的手势优先识别,就像下面这样:

struct ContentView: View {
    var body: some View {
        VStack {
            Text("Hello, World!")
                .onTapGesture {
                    print("文本被点击")
                }
        }
        .highPriorityGesture(
            TapGesture()
                .onEnded { _ in
                    print("VStack被点击")
                }
        )
    }
}

或者你可以使用 simultaneousGesture() modifier 来告诉 SwiftUI,你希望父视图和子视图的手势同时触发,代码如下:

struct ContentView: View {
    var body: some View {
        VStack {
            Text("Hello, World!")
                .onTapGesture {
                    print("文本被点击")
                }
        }
        .simultaneousGesture(
            TapGesture()
                .onEnded { _ in
                    print("VStack被点击")
                }
        )
    }
}

上面的代码在点击时会同时打印 “文本被点击” 和 “VStack被点击”。

最后,SwiftUI 还能让我们创建手势序列,序列中的手势只有前一个手势成功识别之后才会被激活识别。由于手势之间存在引用,所以比较复杂,你不能简单地将它们直接添加到视图。

下面是一个展示手势序列的例子,在长按之后,你可以继而拖拽一个圆:

struct ContentView: View {
    // 圆被拖拽的偏移量
    @State private var offset = CGSize.zero

    // 是否正在被拖拽
    @State private var isDragging = false

    var body: some View {
        // 拖拽手势更新偏移量和 isDragging
        let dragGesture = DragGesture()
            .onChanged { value in self.offset = value.translation }
            .onEnded { _ in
                withAnimation {
                    self.offset = .zero
                    self.isDragging = false
                }
            }

        // 长按手势激活 isDragging
        let pressGesture = LongPressGesture()
            .onEnded { value in
                withAnimation {
                    self.isDragging = true
                }
            }

        // 组合手势强制用户先长按才能触发拖拽
        let combined = pressGesture.sequenced(before: dragGesture)

        // 一个 64x64 的圆,应用组合手势,在被拖拽时放大,并且按照拖拽的位置做出偏移
        return Circle()
            .fill(Color.red)
            .frame(width: 64, height: 64)
            .scaleEffect(isDragging ? 1.5 : 1)
            .offset(offset)
            .gesture(combined)
    }
}

手势是创建流畅,有趣的用户界面的一种极佳手段。不过,确保你向用户展示了手势的工作方式,不然他们可能会感到困惑。


我的公众号 这里有Swift及计算机编程的相关文章,以及优秀国外文章翻译,欢迎关注~

Swift花园微信公众号