【Swift】 UIKit:UIGestureRecognizer和UIView Animation

二十一、手势识别:UIGestureRecognizer

UIGestureRecognizer 是用于识别用户触摸手势 的抽象基类,UIKit 提供多种具体手势子类(如点击、长按、滑动、拖曳、缩放、旋转),通过绑定 target-action 或代理,实现手势与视图的交互,适用于自定义交互(如图片缩放、视图拖曳)。

21.1 核心手势类型与功能

手势类型 核心功能 适用场景
UITapGestureRecognizer 点击(单指/多指、单击/多击) 按钮点击、列表项选中、图片查看
UILongPressGestureRecognizer 长按(指定长按时间) 弹出菜单、删除确认、预览内容
UISwipeGestureRecognizer 滑动(上下左右方向) 页面返回、删除项、显示隐藏菜单
UIPanGestureRecognizer 拖曳(实时跟踪触摸位置) 视图移动、滑块调整、画板绘图
UIPinchGestureRecognizer 缩放(双指捏合/张开) 图片放大缩小、文本字号调整
UIRotationGestureRecognizer 旋转(双指旋转) 图片旋转、图形调整

21.2 关键属性与方法

手势类型 关键属性/方法 作用说明
Tap(点击) numberOfTapsRequired 触发手势需点击的次数(如 2 表示双击)
numberOfTouchesRequired 触发手势需的手指数量(如 2 表示双指点击)
require(toFail:) 依赖其他手势失败后才触发(如单双击冲突时,双击失败才触发单击)
LongPress(长按) minimumPressDuration 触发长按需的最小时间(默认 0.5 秒)
Swipe(滑动) direction 滑动方向(如 .up/.left,需单独创建不同方向的手势)
Pan(拖曳) location(in:) 获取拖曳时的当前位置(相对于指定视图)
Pinch(缩放) scale 缩放比例(1.0 为原尺寸,>1 放大,<1 缩小)
Rotation(旋转) rotation 旋转弧度(正为顺时针,负为逆时针,需转换为角度 弧度×(180/π)
通用 addTarget(_:action:for:) 绑定手势触发的方法(action 为回调 selector)
state 手势状态(如 .began/.changed/.ended,用于区分手势阶段)

21.3 代码逻辑解析

场景1:点击(Tap)与长按(LongPress)

Swift 复制代码
class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // 1. 双指单击手势
        let doubleFingerTap = UITapGestureRecognizer(target: self, action: #selector(doubleTap(_:)))
        doubleFingerTap.numberOfTapsRequired = 1 // 单击
        doubleFingerTap.numberOfTouchesRequired = 2 // 双指
        view.addGestureRecognizer(doubleFingerTap)
        
        // 2. 单指双击手势(依赖双指单击失败才触发)
        let singleFingerDoubleTap = UITapGestureRecognizer(target: self, action: #selector(singleTap(_:)))
        singleFingerDoubleTap.numberOfTapsRequired = 2 // 双击
        singleFingerDoubleTap.numberOfTouchesRequired = 1 // 单指
        singleFingerDoubleTap.require(toFail: doubleFingerTap) // 双指单击失败才触发
        view.addGestureRecognizer(singleFingerDoubleTap)
        
        // 3. 长按手势
        let longPress = UILongPressGestureRecognizer(target: self, action: #selector(longPress(_:)))
        view.addGestureRecognizer(longPress)
    }

    // 双指单击回调
    @objc func doubleTap(_ recognizer: UITapGestureRecognizer) {
        print("双指单击触发")
        printFingersPosition(recognizer) // 打印手指位置
    }

    // 单指双击回调
    @objc func singleTap(_ recognizer: UITapGestureRecognizer) {
        print("单指双击触发")
        printFingersPosition(recognizer)
    }

    // 长按回调(区分开始和结束状态)
    @objc func longPress(_ recognizer: UILongPressGestureRecognizer) {
        if recognizer.state == .began {
            print("长按开始")
        } else if recognizer.state == .ended {
            print("长按结束")
        }
    }

    // 打印手指位置
    func printFingersPosition(_ recognizer: UITapGestureRecognizer) {
        let fingerCount = recognizer.numberOfTouches
        for i in 0..<fingerCount {
            let position = recognizer.location(ofTouch: i, in: view)
            print("第 \(i+1) 指位置:x=\(position.x), y=\(position.y)")
        }
    }
}

场景2:拖曳(Pan)与滑动(Swipe)

Swift 复制代码
class ViewController: UIViewController {
    let fullSize = UIScreen.main.bounds.size
    var draggableView: UIView! // 可拖曳的视图
    var swipeableView: UIView! // 可滑动的视图

    override func viewDidLoad() {
        super.viewDidLoad()
        
        // 1. 可滑动的视图(蓝色,初始在左上角)
        swipeableView = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
        swipeableView.backgroundColor = .blue
        view.addSubview(swipeableView)
        
        // 2. 滑动手势(上下左右四个方向,需单独创建)
        let swipeUp = UISwipeGestureRecognizer(target: self, action: #selector(swipe(_:)))
        swipeUp.direction = .up
        view.addGestureRecognizer(swipeUp)
        
        let swipeLeft = UISwipeGestureRecognizer(target: self, action: #selector(swipe(_:)))
        swipeLeft.direction = .left
        view.addGestureRecognizer(swipeLeft)
        
        let swipeDown = UISwipeGestureRecognizer(target: self, action: #selector(swipe(_:)))
        swipeDown.direction = .down
        view.addGestureRecognizer(swipeDown)
        
        let swipeRight = UISwipeGestureRecognizer(target: self, action: #selector(swipe(_:)))
        swipeRight.direction = .right
        view.addGestureRecognizer(swipeRight)
        
        // 3. 可拖曳的视图(橙色,初始在屏幕中央)
        draggableView = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
        draggableView.backgroundColor = .orange
        draggableView.center = CGPoint(x: fullSize.width * 0.5, y: fullSize.height * 0.5)
        view.addSubview(draggableView)
        
        // 4. 拖曳手势(绑定到可拖曳视图,仅该视图响应)
        let pan = UIPanGestureRecognizer(target: self, action: #selector(pan(_:)))
        pan.minimumNumberOfTouches = 1 // 最少1指
        pan.maximumNumberOfTouches = 1 // 最多1指
        draggableView.addGestureRecognizer(pan)
    }

    // 滑动手势回调:视图移动100pt(边界限制)
    @objc func swipe(_ recognizer: UISwipeGestureRecognizer) {
        let currentCenter = swipeableView.center
        switch recognizer.direction {
        case .up:
            let newY = currentCenter.y - 100
            swipeableView.center = CGPoint(x: currentCenter.x, y: max(newY, 50)) // 不超过顶部50pt
        case .left:
            let newX = currentCenter.x - 100
            swipeableView.center = CGPoint(x: max(newX, 50), y: currentCenter.y) // 不超过左侧50pt
        case .down:
            let newY = currentCenter.y + 100
            swipeableView.center = CGPoint(x: currentCenter.x, y: min(newY, fullSize.height - 50)) // 不超过底部50pt
        case .right:
            let newX = currentCenter.x + 100
            swipeableView.center = CGPoint(x: min(newX, fullSize.width - 50), y: currentCenter.y) // 不超过右侧50pt
        default: break
        }
    }

    // 拖曳手势回调:视图跟随手指移动
    @objc func pan(_ recognizer: UIPanGestureRecognizer) {
        // 获取手指相对于父视图的位置
        let fingerPosition = recognizer.location(in: view)
        // 视图中心设为手指位置
        draggableView.center = fingerPosition
    }
}

场景3:缩放(Pinch)与旋转(Rotation)

Swift 复制代码
class ViewController: UIViewController {
    let fullSize = UIScreen.main.bounds.size
    var pinchImageView: UIImageView! // 可缩放的图片
    var rotationImageView: UIImageView! // 可旋转的图片

    override func viewDidLoad() {
        super.viewDidLoad()
        
        // 1. 可缩放的图片(01.jpg,初始在左上角)
        pinchImageView = UIImageView(image: UIImage(named: "01.jpg"))
        pinchImageView.frame = CGRect(x: 50, y: 50, width: 200, height: 200)
        view.addSubview(pinchImageView)
        
        // 2. 缩放手势(绑定到父视图,整个屏幕可触发)
        let pinch = UIPinchGestureRecognizer(target: self, action: #selector(pinch(_:)))
        view.addGestureRecognizer(pinch)
        
        // 3. 可旋转的图片(02.jpg,初始在屏幕下方中央)
        rotationImageView = UIImageView(image: UIImage(named: "02.jpg"))
        rotationImageView.frame = CGRect(x: 0, y: 0, width: 200, height: 200)
        rotationImageView.center = CGPoint(x: fullSize.width * 0.5, y: fullSize.height * 0.75)
        view.addSubview(rotationImageView)
        
        // 4. 旋转手势(绑定到父视图)
        let rotation = UIRotationGestureRecognizer(target: self, action: #selector(rotation(_:)))
        view.addGestureRecognizer(rotation)
    }

    // 缩放手势回调:限制缩放范围(0.5~2倍)
    @objc func pinch(_ recognizer: UIPinchGestureRecognizer) {
        guard let imageView = pinchImageView else { return }
        let currentFrame = imageView.frame
        
        // 缩放比例(实时更新)
        let scale = recognizer.scale
        // 计算新尺寸
        let newWidth = currentFrame.width * scale
        let newHeight = currentFrame.height * scale
        
        // 限制缩放范围:宽度100~400pt
        if newWidth > 100 && newWidth < 400 {
            imageView.frame = CGRect(
                x: currentFrame.origin.x, 
                y: currentFrame.origin.y, 
                width: newWidth, 
                height: newHeight
            )
        }
        
        // 重置scale(避免累积缩放)
        if recognizer.state == .ended {
            recognizer.scale = 1.0
        }
    }

    // 旋转手势回调:根据弧度旋转图片
    @objc func rotation(_ recognizer: UIRotationGestureRecognizer) {
        guard let imageView = rotationImageView else { return }
        // 旋转弧度(recognizer.rotation 为从初始位置的累积弧度)
        let radian = recognizer.rotation
        // 转换为角度(便于打印)
        let angle = radian * (180 / CGFloat(Double.pi))
        print("旋转角度:\(angle)°")
        
        // 应用旋转变换(相对于图片中心)
        imageView.transform = CGAffineTransform(rotationAngle: radian)
        
        // 重置rotation(可选,根据需求决定是否累积旋转)
        if recognizer.state == .ended {
            recognizer.rotation = 0.0
        }
    }
}

21.4 注意事项

  • 手势冲突 :多个手势可能冲突(如单双击、滑动与拖曳),可通过 require(toFail:) 设定依赖关系,或在代理方法 gestureRecognizer(_:shouldRecognizeSimultaneouslyWith:) 允许同时识别;

  • 手势绑定对象 :手势绑定到哪个视图,只有该视图及其子视图的触摸才会触发手势(如拖曳手势绑定到 draggableView,仅拖曳该视图时触发);

  • 状态区分 :部分手势(如 LongPress、Pinch)有多个状态(.began/.changed/.ended),需根据状态处理逻辑(如长按开始显示菜单,结束隐藏菜单);

  • 坐标系统location(in:) 方法的参数决定坐标相对于哪个视图(如 in: view 为父视图坐标,in: draggableView 为视图自身坐标),需根据需求选择。


二十二、视图动画:UIView Animation

UIView Animation 是用于实现基础视图动画 的 API,无需复杂动画框架,通过 UIView.animate(withDuration:) 系列方法,即可实现视图属性(如位置、大小、透明度、颜色)的平滑过渡,适用于页面切换、控件交互反馈、数据加载动画等场景。

22.1 核心功能

  • 支持多种视图属性动画:bounds(大小)、center(位置)、alpha(透明度)、backgroundColor(背景色)、transform(旋转/缩放/平移);
  • 自定义动画参数:时长、延迟、过渡曲线(如 curveEaseIn/curveEaseOut)、弹簧效果;
  • 组合动画:同时对多个属性执行动画;
  • 动画回调:completion 闭包在动画结束后执行,用于后续操作。

22.2 关键方法与参数

类别 方法/参数 作用说明
基础动画 UIView.animate(withDuration:animations:) 基础动画:指定时长和动画闭包(需修改的视图属性)
带延迟与曲线 UIView.animate(withDuration:delay:options:animations:completion:) 进阶动画:指定延迟时间、过渡曲线、动画闭包、结束回调
弹簧动画 UIView.animate(withDuration:delay:usingSpringWithDamping:initialSpringVelocity:options:animations:completion:) 弹簧动画:usingSpringWithDamping(弹性,0~1,越小越弹)、initialSpringVelocity(初始速度)
动画属性 bounds 视图大小动画(如从 50×50 变为 150×150)
center 视图位置动画(如从左上角移动到右下角)
alpha 透明度动画(如从 1.0 变为 0.25,实现淡入淡出)
backgroundColor 背景色动画(如从黑色变为蓝色)
transform 变换动画(旋转 rotationAngle、缩放 scale、平移 translation
过渡曲线 options: .curveEaseIn 先慢后快(适用于进入动画)
options: .curveEaseOut 先快后慢(适用于退出动画)
options: .curveEaseInOut 先慢后快再慢(默认,适用于大多数场景)

22.3 代码逻辑解析

场景1:单个属性动画(透明度、背景色)

Swift 复制代码
class ViewController: UIViewController {
    let fullSize = UIScreen.main.bounds.size
    var animateLabel: UILabel! // 动画目标视图(标签)
    // 动画参数数组(循环切换)
    let arrAlpha = [0.25, 0.75, 0.5, 1.0]
    let arrBackgroundColor = [UIColor.cyan, UIColor.green, UIColor.orange, UIColor.black]
    var indexAlpha = 0 // 透明度参数索引
    var indexBgColor = 0 // 背景色参数索引

    override func viewDidLoad() {
        super.viewDidLoad()
        createAnimateLabelAndButtons()
    }

    // 创建动画目标标签和控制按钮
    func createAnimateLabelAndButtons() {
        // 1. 动画目标标签(显示"Swift",初始在左上角)
        animateLabel = UILabel(frame: CGRect(x: 0, y: 0, width: 50, height: 50))
        animateLabel.text = "Swift"
        animateLabel.textColor = .white
        animateLabel.textAlignment = .center
        animateLabel.backgroundColor = .black
        animateLabel.center = CGPoint(x: fullSize.width * 0.25, y: fullSize.height * 0.25)
        view.addSubview(animateLabel)
        
        // 2. 透明度动画按钮
        let alphaBtn = UIButton(frame: CGRect(x: 0, y: 0, width: fullSize.width * 0.5, height: 80))
        alphaBtn.setTitle("alpha", for: .normal)
        alphaBtn.setTitleColor(.blue, for: .normal)
        alphaBtn.addTarget(self, action: #selector(AnimateAlpha), for: .touchUpInside)
        alphaBtn.center = CGPoint(x: fullSize.width * 0.75, y: fullSize.height - 2.5 * 80)
        view.addSubview(alphaBtn)
        
        // 3. 背景色动画按钮
        let bgColorBtn = UIButton(frame: CGRect(x: 0, y: 0, width: fullSize.width * 0.5, height: 80))
        bgColorBtn.setTitle("backgroundColor", for: .normal)
        bgColorBtn.setTitleColor(.blue, for: .normal)
        bgColorBtn.addTarget(self, action: #selector(AnimateBackgroundColor), for: .touchUpInside)
        bgColorBtn.center = CGPoint(x: fullSize.width * 0.25, y: fullSize.height - 1.5 * 80)
        view.addSubview(bgColorBtn)
    }

    // 透明度动画(0.5秒,带延迟0.2秒,先慢后快曲线)
    @objc func AnimateAlpha() {
        UIView.animate(withDuration: 0.5, animations: {
            self.animateLabel.alpha = CGFloat(self.arrAlpha[self.indexAlpha])
        }, completion: { _ in
            print("透明度动画完成")
        })
        // 更新索引(循环切换参数)
        indexAlpha = indexAlpha >= 3 ? 0 : indexAlpha + 1
    }

    // 背景色动画(1秒,延迟0.2秒,先慢后快曲线)
    @objc func AnimateBackgroundColor() {
        UIView.animate(withDuration: 1, delay: 0.2, options: .curveEaseIn, animations: {
            self.animateLabel.backgroundColor = self.arrBackgroundColor[self.indexBgColor]
        }, completion: { _ in
            print("背景色动画完成")
        })
        // 更新索引(循环切换参数)
        indexBgColor = indexBgColor >= 3 ? 0 : indexBgColor + 1
    }
}

场景2:位置与变换动画(带弹簧效果)

Swift 复制代码
class ViewController: UIViewController {
    let fullSize = UIScreen.main.bounds.size
    var animateLabel: UILabel!
    let arrCenter = [ // 位置参数数组(四角+中心)
        CGPoint(x: fullSize.width * 0.75, y: fullSize.height * 0.25),
        CGPoint(x: fullSize.width * 0.75, y: fullSize.height * 0.75),
        CGPoint(x: fullSize.width * 0.25, y: fullSize.height * 0.75),
        CGPoint(x: fullSize.width * 0.25, y: fullSize.height * 0.25)
    ]
    let arrTransform = [ // 旋转参数数组(45°、225°、315°、360°)
        CGAffineTransform(rotationAngle: CGFloat(Double.pi * 0.25)),
        CGAffineTransform(rotationAngle: CGFloat(Double.pi * 1.25)),
        CGAffineTransform(rotationAngle: CGFloat(Double.pi * 1.75)),
        CGAffineTransform(rotationAngle: CGFloat(Double.pi * 2))
    ]
    var indexCenter = 0
    var indexTransform = 0

    // 位置动画(带弹簧效果)
    @objc func AnimateCenter() {
        UIView.animate(withDuration: 1.5, delay: 0.1, 
            usingSpringWithDamping: 0.4, // 弹性:0.4(较弹)
            initialSpringVelocity: 0,    // 初始速度:0
            options: .curveEaseInOut, 
            animations: {
                self.animateLabel.center = self.arrCenter[self.indexCenter]
            }, completion: { _ in
                print("位置动画完成")
            }
        )
        indexCenter = indexCenter >= 3 ? 0 : indexCenter + 1
    }

    // 旋转动画(0.5秒)
    @objc func AnimateTransform() {
        UIView.animate(withDuration: 0.5, animations: {
            self.animateLabel.transform = self.arrTransform[self.indexTransform]
        })
        indexTransform = indexTransform >= 3 ? 0 : indexTransform + 1
    }
}

场景3:组合动画(同时修改多个属性)

Swift 复制代码
class ViewController: UIViewController {
    let fullSize = UIScreen.main.bounds.size
    var animateLabel: UILabel!
    let arrBounds = [ // 大小参数数组
        CGSize(width: 100, height: 100),
        CGSize(width: 50, height: 50),
        CGSize(width: 150, height: 150),
        CGSize(width: 50, height: 50)
    ]
    var indexBounds = 0
    var indexAlpha = 0
    var indexBgColor = 0
    var indexCenter = 0
    var indexTransform = 0

    // 组合动画:同时修改大小、透明度、背景色、位置、旋转
    @objc func AnimateAll() {
        let newSize = arrBounds[indexBounds]
        let originCenter = animateLabel.center // 保持中心不变(避免大小变化导致位置偏移)
        
        UIView.animate(withDuration: 0.5, animations: {
            // 1. 大小动画(bounds)
            self.animateLabel.bounds = CGRect(x: 0, y: 0, width: newSize.width, height: newSize.height)
            // 2. 透明度动画
            self.animateLabel.alpha = CGFloat(self.arrAlpha[self.indexAlpha])
            // 3. 背景色动画
            self.animateLabel.backgroundColor = self.arrBackgroundColor[self.indexBgColor]
            // 4. 位置动画
            self.animateLabel.center = self.arrCenter[self.indexCenter]
            // 5. 旋转动画
            self.animateLabel.transform = self.arrTransform[self.indexTransform]
        })
        
        // 更新所有参数索引
        updateIndex("all")
    }

    // 更新参数索引(循环)
    func updateIndex(_ type: String) {
        if type == "all" {
            indexBounds = indexBounds >= 3 ? 0 : indexBounds + 1
            indexAlpha = indexAlpha >= 3 ? 0 : indexAlpha + 1
            indexBgColor = indexBgColor >= 3 ? 0 : indexBgColor + 1
            indexCenter = indexCenter >= 3 ? 0 : indexCenter + 1
            indexTransform = indexTransform >= 3 ? 0 : indexTransform + 1
        }
    }
}

22.4 注意事项

  • 动画属性限制 :仅部分视图属性支持动画(如 frame/bounds/center/alpha/backgroundColor/transform),tag/isHidden 等属性不支持动画;
  • 弹簧参数usingSpringWithDamping 取值范围 0~1,值越小弹性越强;initialSpringVelocity 为初始速度,值越大初始冲力越强;
  • 动画嵌套 :可在 completion 闭包中嵌套另一个动画,实现连续动画(如先移动再缩放);
  • 性能优化 :避免对大量视图同时执行动画,或动画时长过短(<0.1秒),否则可能导致卡顿;复杂动画(如粒子效果)建议使用 Core Animation 而非基础动画。

资料推荐:https://github.com/0voice

相关推荐
D***t13134 分钟前
PHP在API开发中的框架选择
开发语言·php
H***997635 分钟前
Java虚拟现实案例
java·开发语言·vr
Tan_Ying_Y1 小时前
synchronized和ReentrantLock的区别是什么?他们的底层原理是什么?
开发语言·c#
ChineHe1 小时前
Golang并发编程篇002_Go并发基础
开发语言·后端·golang
默恋~微凉1 小时前
shell(八)——WEB与Nginx
开发语言·前端·php
lsx2024061 小时前
Go 语言类型转换
开发语言
t***L2662 小时前
JavaScript在机器学习中的库
开发语言·javascript·机器学习
私人珍藏库3 小时前
利用 iPhone 或 Apple Watch ,自动锁定和解锁 Windows
ios·iphone
勇闯逆流河3 小时前
【C++】C++11(下)
开发语言·c++