You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何通过子视图手势处理器实时平滑更新UIView?

解决DrawingView实时平滑更新的问题

问题根源

你当前的实现有两个核心问题导致更新不流畅:

  1. 约束修改后,系统不会立即更新视图布局,调用setNeedsDisplay时,ImageView的实际位置还没同步,导致draw(_:)里用的还是旧坐标。
  2. DrawingView依赖ViewController的静态变量获取顶点位置,这种耦合方式不仅不安全,还无法实时获取视图的最新位置。

修复方案(两种可选)

方案一:优化现有draw(_:)实现,实时获取顶点位置

直接让DrawingView持有三个顶点ImageView的引用,在绘制时实时读取它们的中心坐标,同时在手势回调里确保布局更新后再触发重绘。

修改后的代码:

class ViewController: UIViewController {
    
    @IBOutlet var drawingView: DrawingView!

    @IBOutlet var majorVertex1: UIImageView!
    @IBOutlet var majorVertex2: UIImageView!
    @IBOutlet var majorVertex3: UIImageView!
    
    var majorVertices: [UIImageView]!
    
    @IBOutlet var majorVertex1XConstraint: NSLayoutConstraint!
    @IBOutlet var majorVertex1YConstraint: NSLayoutConstraint!
    @IBOutlet var majorVertex2XConstraint: NSLayoutConstraint!
    @IBOutlet var majorVertex2YConstraint: NSLayoutConstraint!
    @IBOutlet var majorVertex3XConstraint: NSLayoutConstraint!
    @IBOutlet var majorVertex3YConstraint: NSLayoutConstraint!
    
    var majorVertexXConstraints: [NSLayoutConstraint]!
    var majorVertexYConstraints: [NSLayoutConstraint]!
    
    // 移除静态变量
    var majorVertexXOffsets: [Double]?
    var majorVertexYOffsets: [Double]?
        
    override func viewDidLoad() {
        super.viewDidLoad()
        
        majorVertices = [majorVertex1, majorVertex2, majorVertex3]
        majorVertexXConstraints = [majorVertex1XConstraint, majorVertex2XConstraint, majorVertex3XConstraint]
        majorVertexYConstraints = [majorVertex1YConstraint, majorVertex2YConstraint, majorVertex3YConstraint]
        // 初始化偏移量
        majorVertexXOffsets = majorVertexXConstraints.map { $0.constant }
        majorVertexYOffsets = majorVertexYConstraints.map { $0.constant }
        
        // 给DrawingView传递顶点视图引用
        drawingView.vertexViews = majorVertices
    }

    @IBAction func handlePan(_ gesture: UIPanGestureRecognizer) {
        guard let gestureView = gesture.view as? UIImageView,
              let parentView = gestureView.superview,
              let index = majorVertices.firstIndex(of: gestureView)
        else { return }
        
        let translation = gesture.translation(in: parentView)
        
        switch gesture.state {
        case .began:
            // 记录初始偏移
            majorVertexXOffsets = majorVertexXConstraints.map { $0.constant }
            majorVertexYOffsets = majorVertexYConstraints.map { $0.constant }
            
        case .changed:
            // 更新约束
            majorVertexXConstraints[index].constant = majorVertexXOffsets![index] + translation.x
            majorVertexYConstraints[index].constant = majorVertexYOffsets![index] + translation.y
            
            // 强制更新布局,确保ImageView位置生效
            parentView.layoutIfNeeded()
            // 触发DrawingView重绘
            drawingView.setNeedsDisplay()
            
        case .ended, .cancelled:
            // 修正约束值,确保后续布局正确
            majorVertexXConstraints[index].constant = gestureView.center.x - parentView.frame.width / 2.0
            majorVertexYConstraints[index].constant = gestureView.center.y - parentView.frame.height / 2.0
            
        default:
            break
        }
    }
}

class DrawingView: UIView {
    // 添加顶点视图引用
    var vertexViews: [UIImageView]?
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupView()
    }
    
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        setupView()
    }
    
    func setupView() {
        backgroundColor = .clear
    }
        
    override func draw(_ rect: CGRect) {
        super.draw(rect)
        drawTriangle()
    }
    
    private func drawTriangle() {
        guard let context = UIGraphicsGetCurrentContext(),
              let vertices = vertexViews, vertices.count == 3
        else { return }
        
        // 实时获取每个ImageView的中心坐标
        let points = vertices.map { $0.center }
        
        context.setStrokeColor(UIColor.lightGray.cgColor)
        context.setLineWidth(3)
        
        context.move(to: points[0])
        context.addLine(to: points[1])
        context.addLine(to: points[2])
        context.closePath() // 替代手动回到起点,更简洁
        
        context.strokePath()
    }
}

方案二:用CAShapeLayer替代draw(_:)(更优,性能更好)

直接在DrawingView里创建CAShapeLayer,通过更新图层路径实现平滑动画,无需每次调用setNeedsDisplay。

修改后的代码:

class ViewController: UIViewController {
    // 代码同方案一的ViewController部分,无需改动
}

class DrawingView: UIView {
    var vertexViews: [UIImageView]? {
        didSet {
            // 顶点视图变化时更新路径
            updateTrianglePath()
        }
    }
    
    private let shapeLayer = CAShapeLayer()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupView()
    }
    
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        setupView()
    }
    
    func setupView() {
        backgroundColor = .clear
        shapeLayer.strokeColor = UIColor.lightGray.cgColor
        shapeLayer.lineWidth = 3
        shapeLayer.fillColor = UIColor.clear.cgColor
        layer.addSublayer(shapeLayer)
    }
    
    // 提供外部调用的更新方法
    func updateTrianglePath() {
        guard let vertices = vertexViews, vertices.count == 3 else {
            shapeLayer.path = nil
            return
        }
        
        let path = UIBezierPath()
        let points = vertices.map { $0.center }
        
        path.move(to: points[0])
        path.addLine(to: points[1])
        path.addLine(to: points[2])
        path.close()
        
        shapeLayer.path = path.cgPath
    }
}

然后在ViewController的手势.changed分支里,替换drawingView.setNeedsDisplay()为:

parentView.layoutIfNeeded()
drawingView.updateTrianglePath()

关键说明

  • layoutIfNeeded():强制立即更新视图布局,确保ImageView的位置在重绘/更新路径前已经生效。
  • 移除静态变量:通过直接持有视图引用或属性传递,避免耦合,保证数据实时性。
  • CAShapeLayer方案:相比draw(_:),图层更新更高效,系统会自动处理动画过渡,平滑度更好。

内容的提问来源于stack exchange,提问作者vonbecker

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.06 04:07:10