今日职言:不要浮在表面,而要用心讲故事。

在本章中,你将学会如何使用​​Gestures​​手势构建基本的交互动作。

在我们常见的业务中会用到很多的交互手势,例如:点击按钮打开页面、长按弹出窗口、拖拽移动等等。这些交互手势结合​​SwiftUI​​​的动画效果,也就呈现出了​​iOS​​应用独具匠心的交互动画。

本章节将分成4个部分讲解。

1、​​onTapGesture​​点击手势

2、​​LongPressGesture​​长按手势

3、​​DragGesture​​拖拽手势

4、多种手势组合使用

那我们开始吧。

第一部分:onTapGesture点击手势

首先创建一个新项目,命名为​​SwiftUIGestures​​。

SwiftUI极简教程17:Gestures手势的使用_长按手势

我们尝试完成下下面的UI稿。

SwiftUI极简教程17:Gestures手势的使用_长按手势_02

首先,我们在ContentView.swift中完成基础样式的绘制。

代码如下:

//绘制

ZStack {
Circle()
.frame(width: 200, height: 200)
.foregroundColor(.red)
Image(systemName: "heart.fill")
.foregroundColor(.white)
.font(.system(size: 80))
}

SwiftUI极简教程17:Gestures手势的使用_长按手势_03

我们来分析下UI稿,我们看到这个交互有几种状态:点击时,背景颜色变成灰色,这是第一种状态;点击时,里面的心形变成红色,这是第二种状态;点击时,心形变大了1倍,这是第三种状态;

首先,我们需要定义三种状态,且它们的初始状态都是​​false​​​,那么点击的时候,三个状态由​​false​​​转变成​​true​​。

//定义状态

@State private var circleColorChanged = false
@State private var heartColorChanged = false
@State

接下来,当我们点击的时候,三种状态切换的同时,UI稿会对应切换到第二个效果。我们可以看到我们做了4步:

1、​​circleColorChanged​​背景颜色变化时,背景颜色会在灰色、红色之间切换;

.foregroundColor(circleColorChanged ? Color(.systemGray5) : .red)

2、​​heartColorChanged​​心形颜色变化时,背景颜色会在灰色、红色之间切换;

.foregroundColor(heartColorChanged ? .red : .white)

3、​​heartSizeChanged​​心形大小变化时,大小在初始的1倍变成0.5倍;

.scaleEffect(heartSizeChanged ? 1.0 : 0.5)

4、点击这个​​ZStack​​整个视图时,三个状态同时切换。

//点击手势

.onTapGesture {
self.circleColorChanged.toggle()
self.heartColorChanged.toggle()
self.heartSizeChanged.toggle()
}

好了,我们画完了,预览运行下看下效果:

SwiftUI极简教程17:Gestures手势的使用_拖拽_04

至此,我们完成了基础的​​.onTapGesture​​点击手势的学习。

科普一个知识点。

如果我们需要实现更加复杂的手势,我们需要引用​​.gesture​​​修饰符,在​​.gesture​​​修饰符下我们需要实现​​.onTapGesture​​​点击手势,就只需要使用​​TapGesture()​​方法。

//手势modifier
.gesture(

//点击手势
TapGesture()
.onEnded({
self.circleColorChanged.toggle()
self.heartColorChanged.toggle()
self.heartSizeChanged.toggle()
})
)

我们给视图启用了​​.gesture​​​修饰符手势,里面是一个​​TapGesture()​​​点击手势,当我们点击结束时 ​​.onEnded({})​​时,三个状态进行切换。

运行下模拟器,我们可以看到点击的效果,点击效果和​​.onTapGesture​​点击手势效果一致。

SwiftUI极简教程17:Gestures手势的使用_修饰符_05

第二部分:LongPressGesture长按手势

接下来,我们学习下​​LongPressGesture​​长按手势。

​LongPressGesture​​长按手势比较好理解,也就是按住操作至少1秒才会识别手势操作。

使用的方式也是使用​​.gesture​​​修饰符,在里面引用​​LongPressGesture​​长按手势。

//手势modifier
.gesture(

//长按手势
LongPressGesture(minimumDuration: 1.0)
.onEnded({ _ in
self.circleColorChanged.toggle()
self.heartColorChanged.toggle()
self.heartSizeChanged.toggle()
})
)

SwiftUI极简教程17:Gestures手势的使用_SwiftUI_06

运行模拟器,我们长按住图像视图,发现长按1秒钟后,视图就切换成第二个状态了。

但这不够完美,如果我们需要设置1秒钟以上的长按,比如长按2秒钟才唤起什么操作时,我们发现体验没那么好。这是因为UI没法体现出我们长按的动作。

这时候,我们需要引用一个新的知识点,叫做​​@GestureState​​​属性包装器。和之前的章节学习的​​@State​​一样,它可以监测长按手势的状态,也就是点击的时候它能“知道”。

我们首先先定义一个​​longPressTap​​​长按手势,初始值为​​false​​,当它被点击时,状态就切换。

@GestureState private var longPressTap = false

为了达到明显的演示效果,我们给UI稿中的心形加个透明度的效果,当我们长按时,即​​longPressTap​​被点击时,透明度变化。

.opacity(longPressTap ? 0.4 : 1.0)

再然后,我们需要在​​LongPressGesture​​​长按手势内增加一个​​.updating​​​更新方法,当视图被​​LongPressGesture​​长按时,调用这个方法。

//更新方法
.updating($longPressTap, body: { (

currentState, state, transaction

) in

state

​.updating​​​更新方法有三个参数,​​value、state​​​和​​transaction​​。

​value​​​参数可以自定义,我们这里用的是手势的当前状态​​currentState​​​,​​currentState​​当前状态表示检测到点击。

​state​​​参数实际上是一个​​in-out​​​参数,它允许您更新​​longPressTa​​p属性的值。

在上面的代码中,我们将​​state​​​的值设置为​​currentState​​​当前状态,也就是​​longPressTap​​属性需要一直跟踪长按手势的最新状态。

一句话概括就是:​​state​​​参数存储​​currentState​​​当前状态来处理​​updating​​更新的上下文。

运行下模拟器,我们可以看到我们点击视图的时候,心形透明度降低从而变暗淡了,保持了1秒钟,视图就切换到了第二个状态。

SwiftUI极简教程17:Gestures手势的使用_修饰符_07

使用​​@GestureState​​的好处是,当手势结束时,它会自动将手势状态属性的值设置为初始值。

也就实现了我们只在​​LongPressGesture​​长按那1秒钟内,有一个交互效果,代表了用户正在长按这件事。

第三部分:DragGesture拖拽手势

经过上面学习的​​TapGesture​​​点击手势、​​LongPressGesture​​​长按手势,相信你对​​.gesture​​修饰符学习有了更深的了解。

接下来,我们再学习一个手势,​​DragGesture​​拖拽手势。

//手势modifier
.gesture(

//拖拽手势
DragGesture()

)

使用​​DragGesture​​​拖拽手势前,我们也需要使用​​@GestureState​​​属性包装器定义一个拖拽位置参数​​dragOffset​​​,用来记录我们的拖拽前的初始位置​​CGSize.zero​​,也用来监听和更新UI。

@GestureState private var dragOffset = CGSize.zero

然后我们给整个​​ZSkcak​​视图加一个可以拖动的偏移位置。

//移动位置
.offset(x: dragOffset.width, y: dragOffset.height)

它的​​横轴x​​​为我们定义的拖拽位置参数​​dragOffset​​​的宽度,​​纵轴y​​​为拖拽位置参数​​dragOffset​​的高度,也就是我们上下左右都可以拖动。

再然后和​​LongPressGesture​​​长按手势一样,我们需要再​​DragGesture​​拖拽手势中添加更新方法。

//更新方法
.updating($dragOffset, body: { (

currentPosition, state, transaction

) in

state

​.updating​​​更新方法有三个参数,​​value、state​​​和​​transaction​​。

​value​​​参数我们这里用的是手势的当前位置​​currentPosition​​​,​​currentPosition​​当前位置表示当前被拖动的位置。

然后也是​​state​​​参数存储​​currentPosition​​​当前位置来处理​​updating​​更新的上下文。

简单来说,就是拖动的时候,系统需要试试获取你拖动的位置来更新UI所展示的画面,你拖到那里,视图就移动到哪里。

SwiftUI极简教程17:Gestures手势的使用_Swift_08

科普一个知识点。

由于我们使用​​@GestureState​​属性包装器监听变化,因此拖动结束后,视图还会回到初始位置。

如果我们想要拖动之后,就把视图放在拖拽后的位置,还记得之前的章节么?没错,我们需要使用​​@State​​把拖动后的位置存储起来。

我们再定义一个位置:

@State private var position = CGSize.zero

然后拖拽视图的时候,​​x、y轴​​​的位置需要再原先的基础上再加上我们​​position​​的位置。

//移动位置
.offset(x: position.width + dragOffset.width, y: position.height + dragOffset.height)

然后呢,我们在拖拽结束的时候,记录视图移动后的位置,这样就知道了拖动后视图应该放在哪里了。

而我们使用的​​@State​​​属性包装器定义的​​position​​位置,也就记录并保存了我们的位置,这样就就不会回到初始位置了。

//拖拽结束后的位置
.onEnded({ (currentPosition) in

self.position.height += currentPosition.translation.height
self.position.width += currentPosition.translation.width

})

SwiftUI极简教程17:Gestures手势的使用_修饰符_09

前方高能!

前方高能!

前方高能!

第四部分:多种手势组合使用

上面我们都只讨论了单个手势的使用方法,如果一个视图中有多个手势同时操作时,我们该怎么处理?

比方说,如果我们希望用户在开始拖拽之前按住图像,我们必须结合长按和拖拽手势。

​SwiftUI​​​提供了三种手势组合类型,包括同步的、顺序的和排他的。比如,当需要同时检测多个手势时,可以使用同步合成类型。而且​​SwiftUI​​​可以识别指定的手势,但当检测到其中一种手势时,它会忽略其余的手势。当我们使用序列组合类型组合多个手势,​​SwiftUI​​将按照特定的顺序识别这些手势。

以下面的UI稿为例:

SwiftUI极简教程17:Gestures手势的使用_修饰符_10

我们想要实现的组合交互是:初始状态不变,长按时切换状态,并且长按1秒后可以拖拽,拖拽后停留到拖拽后的位置,且切换样式状态。

相当于将第二部分、第三部分的内容结合一下。

首先,我们在​​.gesture​​修饰符先完成长按手势及其更新方法,同时长按的时候,切换UI样式状态。

// 长按手势
LongPressGesture(minimumDuration: 1.0)

// 长按手势更新方法
.updating($longPressTap, body: {

(currentState, state, transaction) in

state = currentState
self.circleColorChanged.toggle()
self.heartColorChanged.toggle()
self.heartSizeChanged.toggle()

})

然后,​​LongPressGesture​​​长按手势后承接的是​​DragGesture​​​拖拽手势,承接的手势组合顺序的用的修饰符是​​.sequenced​​序列。

.sequenced(before: DragGesture())

紧接着,实现拖拽手势的更新方法。

// 拖拽手势更新方法
.updating($dragOffset, body: {

(currentPosition, state, transaction) in

//顺序执行
switch currentPosition {

case .first(true):
print("正在点击")

case .second(true, let drag):
state = drag?.translation ?? .zero

default:
break

这里我们用​​switch​​​语句来区分手势,使用​​.first​​​和​​.second case​​​来找出要处理的手势,首先识别的是​​LongPressGesture​​​长按手势,再识别​​DragGesture​​拖拽手势。

因为​​LongPressGesture​​​长按手势之前已经被触发了,所以这里就​​print​​打印信息供我们参考吧。

第二个被识别​​DragGesture​​​拖拽手势,我们选取拖动数据并使用相应的转换更新​​dragOffset​​。

识别完成后​​break​​​退出​​switch​​区分判断。

最后,我们只需要当拖动结束时,调用​​onEnded​​函数更新样式就行了。

// 拖拽结束后的位置
.onEnded({ currentPosition in

guard case .second(true, let drag?) = currentPosition else {
return
}

self.position.height += drag.translation.height
self.position.width += drag.translation.width
self.circleColorChanged.toggle()
self.heartColorChanged.toggle()
self.heartSizeChanged.toggle()

})

运行下模拟器,我们尝试下完成的交互效果。

完整代码如下:

import SwiftUI

struct ContentView: View {

// 定义状态
@State private var circleColorChanged = false
@State private var heartColorChanged = false
@State private var heartSizeChanged = false
@GestureState private var longPressTap = false
@GestureState private var dragOffset = CGSize.zero
@State private var position = CGSize.zero

var body: some View {

// 绘制
ZStack {
Circle()
.frame(width: 200, height: 200)
.foregroundColor(circleColorChanged ? Color(.systemGray5) : .red)
Image(systemName: "heart.fill")
.foregroundColor(heartColorChanged ? .red : .white)
.font(.system(size: 80))
.scaleEffect(heartSizeChanged ? 1.0 : 0.5)
}

// 移动位置
.offset(x: position.width + dragOffset.width, y: position.height + dragOffset.height)

// 手势modifier
.gesture(

// 长按手势
LongPressGesture(minimumDuration: 1.0)

// 长按手势更新方法
.updating($longPressTap, body: {

currentState, state, _ in

state = currentState
self.circleColorChanged.toggle()
self.heartColorChanged.toggle()
self.heartSizeChanged.toggle()
})

// 拖拽手势
.sequenced(before: DragGesture())

// 拖拽手势更新方法
.updating($dragOffset, body: {

currentPosition, state, _ in

// 顺序执行
switch currentPosition {

case .first(true):
print("正在点击")

case .second(true, let drag):
state = drag?.translation ?? .zero

default:
break

}
})

// 拖拽结束后的位置
.onEnded({ currentPosition in
guard case .second(true, let drag?) = currentPosition else {
return
}
self.position.height += drag.translation.height
self.position.width += drag.translation.width
self.circleColorChanged.toggle()
self.heartColorChanged.toggle()
self.heartSizeChanged.toggle()
})
)
}
}

struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

SwiftUI极简教程17:Gestures手势的使用_拖拽_11

快来动手试试吧!

如果本专栏对你有帮助,不妨点赞、评论、关注~