【Swift】 UIKit:UIGestureRecognizer和UIView Animation

Swift中手势与动画实战

二十一、手势识别: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)

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)

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)

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:单个属性动画(透明度、背景色)

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:位置与变换动画(带弹簧效果)

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:组合动画(同时修改多个属性)

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

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值