在本章中,你将学会如何使用Gestures
手势构建基本的交互动作。
在我们常见的业务中会用到很多的交互手势,例如:点击按钮打开页面、长按弹出窗口、拖拽移动等等。这些交互手势结合SwiftUI
的动画效果,也就呈现出了iOS
应用独具匠心的交互动画。
本章节将分成4个部分讲解。
1、onTapGesture
点击手势
2、LongPressGesture
长按手势
3、DragGesture
拖拽手势
4、多种手势组合使用
那我们开始吧。
第一部分:onTapGesture点击手势
首先创建一个新项目,命名为SwiftUIGestures
。
我们尝试完成下下面的UI稿。
首先,我们在ContentView.swift中完成基础样式的绘制。
代码如下:
//绘制 ZStack { Circle() .frame(width: 200, height: 200) .foregroundColor(.red) Image(systemName: "heart.fill") .foregroundColor(.white) .font(.system(size: 80)) }
我们来分析下UI稿,我们看到这个交互有几种状态:点击时,背景颜色变成灰色,这是第一种状态;点击时,里面的心形变成红色,这是第二种状态;点击时,心形变大了1倍,这是第三种状态;
首先,我们需要定义三种状态,且它们的初始状态都是false
,那么点击的时候,三个状态由false
转变成true
。
//定义状态 @State private var circleColorChanged = false @State private var heartColorChanged = false @State private var heartSizeChanged = false
接下来,当我们点击的时候,三种状态切换的同时,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() } 复制代码
好了,我们画完了,预览运行下看下效果:
至此,我们完成了基础的.onTapGesture
点击手势的学习。
科普一个知识点。
如果我们需要实现更加复杂的手势,我们需要引用.gesture
修饰符,在.gesture
修饰符下我们需要实现.onTapGesture
点击手势,就只需要使用TapGesture()
方法。
//手势modifier .gesture( //点击手势 TapGesture() .onEnded({ self.circleColorChanged.toggle() self.heartColorChanged.toggle() self.heartSizeChanged.toggle() }) )
我们给视图启用了.gesture
修饰符手势,里面是一个TapGesture()
点击手势,当我们点击结束时 .onEnded({})
时,三个状态进行切换。
运行下模拟器,我们可以看到点击的效果,点击效果和.onTapGesture
点击手势效果一致。
第二部分:LongPressGesture长按手势
接下来,我们学习下LongPressGesture
长按手势。
LongPressGesture
长按手势比较好理解,也就是按住操作至少1秒才会识别手势操作。
使用的方式也是使用.gesture
修饰符,在里面引用LongPressGesture
长按手势。
//手势modifier .gesture( //长按手势 LongPressGesture(minimumDuration: 1.0) .onEnded({ _ in self.circleColorChanged.toggle() self.heartColorChanged.toggle() self.heartSizeChanged.toggle() }) )
运行模拟器,我们长按住图像视图,发现长按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 = currentState })
.updating
更新方法有三个参数,value、state
和transaction
。
value
参数可以自定义,我们这里用的是手势的当前状态currentState
,currentState
当前状态表示检测到点击。
state
参数实际上是一个in-out
参数,它允许您更新longPressTa
p属性的值。
在上面的代码中,我们将state
的值设置为currentState
当前状态,也就是longPressTap
属性需要一直跟踪长按手势的最新状态。
一句话概括就是:state
参数存储currentState
当前状态来处理updating
更新的上下文。
运行下模拟器,我们可以看到我们点击视图的时候,心形透明度降低从而变暗淡了,保持了1秒钟,视图就切换到了第二个状态。
使用@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 = currentPosition.translation })
.updating
更新方法有三个参数,value、state
和transaction
。
value
参数我们这里用的是手势的当前位置currentPosition
,currentPosition
当前位置表示当前被拖动的位置。
然后也是state
参数存储currentPosition
当前位置来处理updating
更新的上下文。
简单来说,就是拖动的时候,系统需要试试获取你拖动的位置来更新UI所展示的画面,你拖到那里,视图就移动到哪里。
科普一个知识点。
由于我们使用@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
提供了三种手势组合类型,包括同步的、顺序的和排他的。比如,当需要同时检测多个手势时,可以使用同步合成类型。而且SwiftUI
可以识别指定的手势,但当检测到其中一种手势时,它会忽略其余的手势。当我们使用序列组合类型组合多个手势,SwiftUI
将按照特定的顺序识别这些手势。
以下面的UI稿为例:
我们想要实现的组合交互是:初始状态不变,长按时切换状态,并且长按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() } }
快来动手试试吧!