iOS开发 SwiftUI 15:手势 拖动 缩放 旋转

苹果公司做对了很重要的一点:移动设备要为手指设计(你看,我是个很客观的人,不总是说苹果的坏话)。

相比而言,windows的移动化的一大失败就是手指很难精确点中狭小的菜单,而通过长按呼出右键菜单也完全失去了"快捷"的意义。

目录

[.onTapGesture 点击](#.onTapGesture 点击)

[.onLongPressGesture 长按](#.onLongPressGesture 长按)

[DragGesture 拖动 任意移动对象](#DragGesture 拖动 任意移动对象)

[MagnificationGesture 缩放](#MagnificationGesture 缩放)

[RotationGesture 旋转](#RotationGesture 旋转)

手势组合


.onTapGesture 点击

点击很容易理解,就是鼠标单击。点击和Button有什么区别?Button按下有默认效果处理,点击就是点击,没有效果的。

点击可以用.onTapGesture修饰,最简单。也可以用比较复杂的.gesture,.gesture里面可以综合综合多种手势。

因为太简单,我们代码后面再列。

.onLongPressGesture 长按

长按可以理解为对应鼠标右键,长按有参数控制最短时间和手指移动最大距离,不过一般来讲不需要修改,遵循系统就可以了。长按可以区分按下和结束,按下会被不断触发,可以搞点动画,结束处理和单击差不多。按下和结束通过bool参数来区分。

下面是点击和长按的代码:

Swift 复制代码
    @State var str = "info"

    var body: some View {
        VStack {
            Spacer()
            Text(str)
                .padding()
                .border(.blue)
                .onTapGesture {
                    str = "Tap Text"
                }
                .onLongPressGesture(pressing: { b in
                    if b { str = "...... Text" }
                }) {
                    str = "LongPress Text"
                }

            Spacer()
                .background(.green)  //没用
                .border(.green)  //没用
                .onTapGesture {  //没用,Spacer啥都不支持
                    str = "Tap Spacer"
                }
        }
        .background(.green)  //背景区域都有效
        .border(.green, width: 5)  //仅对边框有效,但是当边框位置改变时,原来的位置仍然有效,或许是BUG
        .onTapGesture {
            str = "Tap VStack"
        }
    }

代码里对不同视图都做了点击处理。实测Spacer是完全不响应的,设置边界和背景也没有意义,也就是Spacer只是用来改变布局的。对VStack,设置背景的话整个背景都有效,设置边界则只有边界上才有效,而且,当边界改变时,新边界有效,原来的边界位置也仍然有效,不晓得是feature还是BUG。

程序很简单,都是改变文本内容,初始:

点击:

长按过程中截不到图,效果是有的。

DragGesture 拖动 任意移动对象

注意是拖动,不是拖放,onDrag是拖放,没有onDragGesture,只能用.gesture(DragGesture())来实现。

拖动最简单的应用就是改变对象位置,然而可笑的是,书上和网上的例子试了都有问题,手指放开就会回到原位,下面的代码是我重新琢磨的:

Swift 复制代码
    @State var offset = CGSize.zero
    @State var lastoffset = CGSize.zero

//in body
            Text(str)//长按下面增加两个修饰
                .offset(offset)
                .gesture(
                    DragGesture()
                        .onChanged { Value in
                            offset.width =
                                lastoffset.width + Value.translation.width
                            offset.height =
                                lastoffset.height + Value.translation.height
                        }
                        .onEnded { Value in
                            lastoffset.width += Value.translation.width
                            lastoffset.height += Value.translation.height
                        }
                )

.offset用来指示位置移动,参数是移动多少,就是说每次绑定变量offset改变时触发.offset修饰,基于原始位置移动对象。DragGesture的onChanged和onEnded参数都是DragGesture.Value,包含了手势的相关参数,包括起始位置、当前位置、当前偏移量预测结束位置、预测结束偏移量和持续时间。由于.offset的参数是偏移量,因此我们用了translation。但是每次拖动时translation都是基于本次拖动,所以需要额外一个变量来记住上次拖动最后停留在哪里了。

代码中lastoffset用来记录本次拖动之前的偏移量,offset则是本次之前的偏移量加上当前拖动的偏移量。

效果,原始位置:

拖到左上角:

再拖到右下角:

我们注意到文本变成了省略号,这是在长按过程中设置的。刚开始动作被识别为长按,然后变成了拖动,因此最后长按结束动作不会被触发。

MagnificationGesture 缩放

缩放和拖动一样,没有独立的onMagnificationGesture,只能在.gesture处理。.gesture如果需要同时处理多个手势,需要使用SimultaneousGesture。如果希望只识别优先级高的,则使用ExclusiveGesture。

下面的代码同时支持拖动和缩放:

Swift 复制代码
    @State var scale: CGFloat = 1.0
                
                .scaleEffect(scale)
                .gesture(
                    SimultaneousGesture( //ExclusiveGesture
                        DragGesture()
                            .onChanged { Value in
                                offset.width =
                                    lastoffset.width + Value.translation.width
                                offset.height =
                                    lastoffset.height + Value.translation.height
                            }
                            .onEnded { Value in
                                lastoffset.width += Value.translation.width
                                lastoffset.height += Value.translation.height
                            },
                        MagnificationGesture()
                            .onChanged { value in
                                scale = value
                            }
                    )
                )

这段代码在预览里不太容易做出缩放效果,需要借助option键,而且由于Text视图比较小,容易因为系统认为的两指位置超出范围而无效,但在实体机上是比较容易的。由于同时支持了拖动,缩放完位置也会变。

.scaleEffect控制视图的缩放效果,参数是缩放比例,onChanged的参数也是缩放比例,所以只要把参数复制给绑定变量就行了。

效果:

与拖动一样,也有onEnded可供处理。

RotationGesture 旋转

旋转是两指旋转(而不是单指)。代码也完全类似缩放,.rotationEffect修饰视图的旋转,参数是角度,类型为Angle。角度分角度和弧度两种,不过只影响Angle类型的使用,我们这里直接从onChanged参数复制,不涉及具体计算。

由于ExclusiveGesture只支持两种手势组合,所以需要用.highPriorityGesture代替.gesture,这两个的区别顾名思义,一个是高优先级,一个是默认优先级。

代码:

Swift 复制代码
    @State private var angle: Angle = Angle(degrees: 90)  //.zero // 旋转角度


                .rotationEffect(angle)
                .gesture(
                    。。。。。。
                )
                .highPriorityGesture(
                    RotationGesture()
                        .onChanged { value in
                            angle = value
                        }
                )

手势组合

前面我们已经使用了不同的手势组合方法,实话说有点乱。

下面是本篇的完整代码:

Swift 复制代码
import SwiftUI

struct SwiftUIViewGesture: View {
    @State var str = "info"  //跟踪信息
    @State var offset = CGSize.zero  //偏移量,基于原始位置
    @State var lastoffset = CGSize.zero  //拖动时的基础偏移量
    @State var scale: CGFloat = 1.0  //放大倍数
    @State private var angle: Angle = Angle(degrees: 90)  //.zero // 旋转角度

    var body: some View {
        VStack {
            Spacer()
            Text(str)
                .padding()
                .border(.blue)
                .onTapGesture {
                    str = "Tap Text"
                }
                .onLongPressGesture(pressing: { b in
                    if b { str = "...... Text" }
                }) {
                    str = "LongPress Text"
                }
                .offset(offset)
                .scaleEffect(scale)
                .rotationEffect(angle)
                .gesture(
                    SimultaneousGesture(  //仅处理优先级高的
                        //ExclusiveGesture(//同时处理
                        DragGesture()
                            .onChanged { Value in
                                offset.width =
                                    lastoffset.width + Value.translation.width
                                offset.height =
                                    lastoffset.height + Value.translation.height
                            }
                            .onEnded { Value in
                                lastoffset.width += Value.translation.width
                                lastoffset.height += Value.translation.height
                            },
                        MagnificationGesture()
                            .onChanged { value in
                                scale = value
                            }
                    )
                )
                .highPriorityGesture(
                    RotationGesture()
                        .onChanged { value in
                            angle = value
                        }
                )

            Spacer()
                .background(.green)  //没用
                .border(.green)  //没用
                .onTapGesture {  //没用,Spacer啥都不支持
                    str = "Tap Spacer"
                }
        }
        .background(.green)  //背景区域都有效
        .border(.green, width: 5)  //仅对边框有效,但是当边框位置改变时,原来的位置仍然有效,或许是BUG
        .onTapGesture {
            str = "Tap VStack"
        }
    }
}

#Preview {
    SwiftUIViewGesture()
}
相关推荐
忙碌5444 小时前
OpenTelemetry实战指南:构建云原生全链路可观测性体系
ios·flink·apache·iphone
升讯威在线客服系统5 小时前
从 GC 抖动到稳定低延迟:在升讯威客服系统中实践 Span 与 Memory 的高性能优化
java·javascript·python·算法·性能优化·php·swift
阿捏利6 小时前
详解Mach-O(十五)Mach-O __DATA_CONST
macos·ios·c/c++·mach-o
Swift社区7 小时前
LeetCode 390 消除游戏 - Swift 题解
leetcode·游戏·swift
肖老师xy8 小时前
uniapp ios离线打包后xcode修改
ios·uni-app
wangyang62758 小时前
Xcode 26 真机运行崩溃 EXC_BAD_ACCESS map_images_nolock 完美解决方案
flutter·ios
编程之路从0到118 小时前
ReactNative新架构之iOS端TurboModule源码剖析
react native·ios·源码阅读
SameX1 天前
春节期间独立开发者从 0 到 1:呼吸训练 iOS App 的工程化落地
ios