案例分析
本文主要讲解一个DragGesture手势的代码样例,了解DragGesture是如何运作的。
示例代码
struct ContentView: View {
@State private var offset = CGSize.zero // 偏移量
var body: some View {
Circle()
.fill(Color.blue)
.frame(width: 100, height: 100)
.offset(offset)
.gesture(
DragGesture()
.onChanged { value in
// 限制偏移量在 -100 到 100 之间
offset = CGSize(
width: min(max(value.translation.width, -100), 100),
height: min(max(value.translation.height, -100), 100)
)
}
.onEnded { _ in
// 松手后复位
withAnimation {
offset = .zero
}
}
)
}
}
这段代码的效果为:使用DragGesture手势拖动圆圈,会被限制到200 * 200的区间,当松手时,会恢复原位。

代码分析
1、@State变量
@State private var offset = CGSize.zero // 偏移量
定义一个 @State 状态变量 offset,表示视图的偏移量。
CGSize是一个结构体:
struct CGSize {
var width: CGFloat
var height: CGFloat
}
默认值是 .zero,表示初始位置没有偏移,等价于 CGSize(width: 0, height: 0)。
2、创建Circle()
Circle()
.fill(Color.blue)
.frame(width: 100, height: 100)
创建一个蓝色的圆形,大小为 100×100。
3、offset(offset)
Circle()
.offset(offset)
设置视图的偏移量。
offset 是一个 CGSize 类型,包含 width 和 height 两个值,分别表示视图在水平方向(X 轴)和竖直方向(Y 轴)的偏移量。
当@State变量offset的width和height变更时,Circle的位置对应的会发生偏移。
4、DragGesture手势和回调方法
.gesture(
DragGesture()
.onChanged { value in
// 限制偏移量在 -100 到 100 之间
offset = CGSize(
width: min(max(value.translation.width, -100), 100),
height: min(max(value.translation.height, -100), 100)
)
}
.onEnded { _ in
// 松手后复位
withAnimation {
offset = .zero
}
}
)
这段代码定义了一个DragGesture拖动手势
onChanged 回调
.onChanged { value in
offset = CGSize(
width: min(max(value.translation.width, -100), 100),
height: min(max(value.translation.height, -100), 100)
)
}
1、触发时机:
当用户拖动视图时,onChanged 会被连续调用,并实时更新拖动的偏移量。
2、参数:value
value.translation 是一个 CGSize,表示从手势起始点到当前位置的偏移量。
3、逻辑解析:
value.translation.width 和 value.translation.height 表示拖动手势的水平和垂直偏移量。

偏移量的正负取决于拖动方向,与屏幕的坐标系一致:
水平轴(X 轴):
向右为正,向左为负。
垂直轴(Y 轴):
向下为正,向上为负。
通过 min(max(…)) 对偏移量进行限制,确保它不会超出 [-100, 100] 的范围:
max(value.translation.width, -100):限制最小值为 -100。
min(…, 100):限制最大值为 100。
offset 会被实时更新,从而控制视图的偏移位置。

当将圆圈向左拖动时,width和height会自动计算:
width: min(max(value.translation.width, -100), 100),
height: min(max(value.translation.height, -100), 100)
当value.translation.width为-200时:
max(value.translation.width, -100)
这里的max会计算最大值,因为-100大于-200,因此,这里返回的是-100。
min(max(value.translation.width, -100), 100)
上面的max返回-100,外层的min计算的是最小值,-100比100小,所以最后的width就是-100。

因此,圆圈会被限制在-100区间内,当手势向左超过-100时,width经过min(max( _, _))的计算后,得到的是-100。
.onChanged { value in
offset = CGSize(
width: -100, // 伪代码
)
}
因此,onChanged会将offset的width设置为-100,即使手势向左到-200、-300,但是offset的width只会在-100以内,height也同理。
onEnded 回调
.onEnded { _ in
// 松手后复位
withAnimation {
offset = .zero
}
}
触发时机:
当用户停止拖动(手势结束)时调用。
逻辑解析:
使用 withAnimation 包裹,将 offset 的值重置为 .zero。
这会通过动画效果使视图平滑地回到初始位置。
当松开手势时,offset会被设置为.zero,表示视图的初始位置。

.zero 的含义是指视图的初始位置,即相对于视图布局的 (0, 0) 坐标位置。
在 SwiftUI 中,视图的 .offset 修改器是以其初始位置为参考点的。所以:
offset = .zero 将会把视图的偏移量复位到视图的初始位置(即未偏移时的位置)。
.zero 与手势的开始位置(DragGesture 的起点)无关,而是绝对的初始偏移量。
进阶代码
struct ContentView: View {
@State private var position = CGSize.zero
@State private var dragOffset = CGSize.zero
var body: some View {
Circle()
.fill(Color.blue)
.frame(width: 100, height: 100)
.offset(x: position.width + dragOffset.width, y: position.height + dragOffset.height)
.gesture(
DragGesture()
.onChanged { value in
dragOffset = value.translation // 实时更新
}
.onEnded { value in
position.width += value.translation.width
position.height += value.translation.height
dragOffset = .zero
}
)
}
}

这段代码与前面的代码相比,圆形会随着手指拖动,并在释放手指后保持最终位置。
这里主要在于两个变量的计算:
@State private var position = CGSize.zero
@State private var dragOffset = CGSize.zero
其中position表示圆圈的定位,dragOffset表示偏移的位置,每次拖动时,都将圆圈的偏移位置加到postion上,这样,圆圈就会保留在偏移后的位置。
关键代码
Circle()
.offset(x: position.width + dragOffset.width, y: position.height + dragOffset.height)
.gesture(
DragGesture()
.onChanged { value in
dragOffset = value.translation // 实时更新
}
.onEnded { value in
position.width += value.translation.width
position.height += value.translation.height
dragOffset = .zero
}
)
圆圈的偏移量是通过positon和dragOffset相加计算的:
.offset(x: position.width + dragOffset.width, y: position.height + dragOffset.height)
DragGesture使用onChanged和onEnded两个回调方法:
DragGesture()
.onChanged { value in
dragOffset = value.translation // 实时更新
}
.onEnded { value in
position.width += value.translation.width
position.height += value.translation.height
dragOffset = .zero
}
当移动圆圈时,dragOffset被赋值为手势移动的偏移量,当向左上角移动时,假设移动的偏移量为(-100,100)。

这是dragOffset就是偏移量的值:
dragOffset = CGSize(width: -100, height: 100)
这也意味着,圆圈的位置会随着dragOffset的变化而变化。
假设在这个偏移量(-100,100)上松开手势,DragGesture的偏移量会被加到叠加到position上。
position.width += value.translation.width
position.height += value.translation.height
这时的position就从0变成了(-100,100),然后dragOffset为赋值为.zero,表示偏移量为0。
因为圆圈的位置是根据positon和dragOffset相加计算的,所以,当position为(-100,100),dragOffset为重置为0时,圆圈的位置就变成(-100,0)。
因此,圆圈就是在手势松开的位置停止。
可以总结为:position为圆圈的每次初始位置,dragOffset为每次的偏移量。第一次小球偏移了(-100,100),onChanged会实时计算偏移量的位置,并将偏移量赋值给dragOffset,这样小球就会跟着偏移量移动。
当松开小球时,onEnded会将偏移量叠加到position位置上,这样,小球的初始位置就从(0,0)变成了(-100,100),而dragOffset偏移量置为0,以便小球进行下一次的偏移计算。
再延伸一点,小球当前的位置为(-100,100),当手势向右拖动(200,0)时。

position: (-100,100)
dragOffset:随手势变化(这里的dragOffset为200,0)
这时,小球的偏移量由positon和dragOffset相加计算:
.offset(x: position.width + dragOffset.width, y: position.height + dragOffset.height)
小球位置的伪代码可以写作
x:-100 + 200 = 100,y:100 + 0 = 100
因此当前的小球位置经过positon和dragOffset变成了(100,100)。
这里松开小球后,手势的偏移量(200,0)会被叠加到position上:
position.width += value.translation.width
position.height += value.translation.height
dragOffset = .zero
小球位置的position伪代码可以写作
x:-100 + 200 = 100,y:100 + 0 = 100
与前面的计算是一样的,然后dragOffset置为0。
.offset(x: position.width + dragOffset.width, y: position.height + dragOffset.height)
就变成了:
x: 100 + 0, y: 100 + 0
最后小球的位置就变成了100,100,停止在手势松开的位置上。
相关文章
1、Swift二维空间尺寸CGSize:https://fangjunyu.com/2024/12/12/swift%e4%ba%8c%e7%bb%b4%e7%a9%ba%e9%97%b4%e5%b0%ba%e5%af%b8cgsize/
2、SwiftUI偏移修饰符offset:https://fangjunyu.com/2024/12/13/swiftui%e5%81%8f%e7%a7%bb%e4%bf%ae%e9%a5%b0%e7%ac%a6offset/
3、SwiftUI拖动操作DragGesture:https://fangjunyu.com/2024/12/13/swiftui%e6%8b%96%e5%8a%a8%e6%93%8d%e4%bd%9cdraggesture/