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

iOS Swift中如何通过控制点调整UIBezierPath绘制图形的大小?

需求:通过控制点调整UIBezierPath绘制图形的大小

已实现以下功能:

  • 用UIBezierPath绘制圆形、直线、自由线等图形
  • 图形选中与拖拽移动效果

需要新增功能:通过func drawPoints(bounds:CGRect)中的topRight和bottomRight控制点,调整圆形和直线的大小。

现有代码如下:

import UIKit

enum ShapeType {
    case circle, rectangle, line, pencil
}

class DrawingView: UIView {
    
    private var path = UIBezierPath()
    private var startPoint: CGPoint?
    private var shapeType: ShapeType?
    private var strokeColor = #colorLiteral(red: 0.2274509804, green: 0.7098039216, blue: 0.2862745098, alpha: 1)
    private var lineWidth: CGFloat = 3.0
    private var paths: [(path: UIBezierPath, color: UIColor)] = []
    private var selectedPathIndex: Int?
    private var offset = CGPoint.zero

    override init(frame: CGRect) {
        super.init(frame: frame)
        setupView()
    }
    
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        setupView()
    }
    
    private func setupView() {
        backgroundColor = .clear
        isMultipleTouchEnabled = false
        
    }
        
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        guard let touch = touches.first else { return }
        startPoint = touch.location(in: self)
        
        print("selectPath ==> \(point)")
        selectedPathIndex = nil
        setNeedsDisplay()
        for (index, path) in paths.enumerated() {
            if path.path.bounds.contains(startPoint ?? CGPoint()){
                selectedPathIndex = index
                self.path = path.path
                print(offset)
                offset = CGPoint(x: (startPoint?.x ?? 0.0) - path.path.bounds.origin.x, y: (startPoint?.y ?? 0) - path.path.bounds.origin.y)
                print(offset)
                print("did touch index", selectedPathIndex)
                setNeedsDisplay()
                return
            }
        }
        
        path = UIBezierPath()
        
        if shapeType == .pencil {
            path.move(to: startPoint!)
        }
    }
    
    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
        guard let touch = touches.first, let shapeType = shapeType else { return }
        let currentPoint = touch.location(in: self)
        print(currentPoint)
        if let index = selectedPathIndex {
            let newOrigin = CGPoint(x: currentPoint.x - offset.x, y: currentPoint.y - offset.y)
            print(newOrigin)
            let translation = CGAffineTransform(translationX: newOrigin.x - path.bounds.origin.x, y: newOrigin.y - path.bounds.origin.y)
            print(translation)
            self.paths[index].path.apply(translation)
            setNeedsDisplay()
        }else{
            if shapeType == .pencil {
                if selectedPathIndex != nil{
                    path.move(to: currentPoint)
                }else{
                    path.addLine(to: currentPoint)
                }
               
               
                setNeedsDisplay()
            } else {
                path.removeAllPoints()
                
                guard let startPoint = startPoint else { return }
                
                switch shapeType {
                case .line:
                    path.move(to: startPoint)
                    path.addLine(to: currentPoint)
                case .rectangle:
                    path = UIBezierPath(rect: CGRect(origin: startPoint, size: CGSize(width: currentPoint.x - startPoint.x, height: currentPoint.y - startPoint.y)))
                case .circle:
                    let radius = hypot(currentPoint.x - startPoint.x, currentPoint.y - startPoint.y)
                    path = UIBezierPath(arcCenter: startPoint, radius: radius, startAngle: 0, endAngle: 2 * .pi, clockwise: true)
                case .pencil:
                    break
                }
                setNeedsDisplay()
        }
      }
    }
    
    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
        guard let shapeType = shapeType else { return }
        if self.selectedPathIndex == nil{
            if shapeType != .pencil {
                paths.append((path.copy() as! UIBezierPath, strokeColor))
            } else {
                paths.append((path, strokeColor))
            }
        }
        setNeedsDisplay()
    }
    
    override func draw(_ rect: CGRect) {
        for pathData in paths {
            pathData.color.setStroke()
            pathData.path.lineWidth = lineWidth
            pathData.path.stroke()
        }
        
        strokeColor.setStroke()
        path.lineWidth = lineWidth
        path.stroke()
        
        if selectedPathIndex != nil{
            for (index, pathData) in paths.enumerated() {
                pathData.color.setStroke()
                pathData.path.lineWidth = lineWidth
                pathData.path.stroke()
                
                if let selectedIndex = selectedPathIndex, selectedIndex == index {
                    let bounds = path.bounds
                    // Draw a rectangle around the selected path
                    if isPencil(path: path) {
                        print("This path is a line.")
                        self.drawSquare(bounds: bounds)
                    } else if isCurve(path: path) {
                        print("This path is a curve.")
                        self.drawSquare(bounds: bounds)
                        self.drawPoints(bounds: bounds)
                    } else if isLine(path: path){
                        self.drawPoints(bounds: bounds)
                    }
                }
            }
        }
    }
    
    func drawSquare(bounds:CGRect){
        let selectionRect = UIBezierPath(rect: bounds) // Create a rectangle slightly larger than the path
        #colorLiteral(red: 0.1490196078, green: 0.5019607843, blue: 0.9215686275, alpha: 1).setStroke()
        selectionRect.lineWidth = 1 // Set the line width for the rectangle
        selectionRect.stroke() // Draw the rectangle
    }
       
    func drawPoints(bounds:CGRect){
        let topRight = CGPoint(x: bounds.maxX, y: bounds.minY)
        let bottomRight = CGPoint(x: bounds.minX, y: bounds.maxY)
        
        self.drawPoint(at: topRight, color: #colorLiteral(red: 0.1490196078, green: 0.5019607843, blue: 0.9215686275, alpha: 1)) // Change color and size as needed
        self.drawPoint(at: bottomRight, color: #colorLiteral(red: 0.1490196078, green: 0.5019607843, blue: 0.9215686275, alpha: 1))
    }
       
    func drawPoint(at point: CGPoint, color: UIColor) {
        let pointSize: CGFloat = 16
        let pointRect = CGRect(x: point.x - pointSize / 2, y: point.y - pointSize / 2, width: pointSize, height: pointSize)
        let pointPath = UIBezierPath(ovalIn: pointRect)
        color.setFill()
        UIColor.white.setStroke()
        pointPath.lineWidth = 5
        pointPath.stroke()
        color.setStroke()
        pointPath.fill()
    }
    
    func clear() {
        path.removeAllPoints()
        paths.removeAll()
        setNeedsDisplay()
    }
    
    func setStrokeColor(_ color: UIColor) {
        strokeColor = color
    }
    
    func setLineWidth(_ width: CGFloat) {
        lineWidth = width
    }
    
    func setShapeType(_ shape: ShapeType) {
        shapeType = shape
    }
    
    func undo() {
        if !paths.isEmpty {
            path.removeAllPoints()
            paths.removeLast()
            self.setNeedsDisplay()
        }
        
    }
    
    func isLine(path: UIBezierPath) -> Bool {
        var isLine = true
        path.cgPath.applyWithBlock { element in
            let type = element.pointee.type
            if type == .addCurveToPoint || type == .addQuadCurveToPoint {
                isLine = false
            }
        }
        return isLine
    }

    
    func isPencil(path: UIBezierPath) -> Bool {
        var isLine = true
        var isLineCount = 0
        path.cgPath.applyWithBlock { element in
            let type = element.pointee.type
            isLineCount += 1
            if type == .addCurveToPoint || type == .addQuadCurveToPoint {
                isLine = false
            }
        }
        return isLine && isLineCount > 2
    }
    
    func isCurve(path: UIBezierPath) -> Bool {
        var isCurve = false

        path.cgPath.applyWithBlock { element in
            let type = element.pointee.type
            if type == .addCurveToPoint || type == .addQuadCurveToPoint {
                isCurve = true
            }
        }

        return isCurve
    }
}

实现方案

1. 重构数据存储结构

直接存储UIBezierPath无法逆向获取图形的原始参数(比如直线的端点、圆形的圆心),需要新增结构体保存图形的完整信息:

// 新增枚举标记控制点类型
enum ControlPointType {
    case topRight, bottomRight
}

// 新增结构体存储图形完整数据
struct ShapeData {
    let type: ShapeType
    var parameters: [CGPoint] // 存储图形原始参数:直线存[起点,终点],圆形存[圆心,半径参考点]
    var path: UIBezierPath
    let color: UIColor
}

// 替换原paths数组
private var shapes: [ShapeData] = []
// 新增变量记录当前拖拽的控制点
private var draggingControlPoint: ControlPointType?
private var originalShape: ShapeData?

2. 修改touchesBegan逻辑

优先判断是否点击了控制点,再处理图形选中:

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
    guard let touch = touches.first else { return }
    let touchPoint = touch.location(in: self)
    
    // 检查是否点击控制点
    draggingControlPoint = nil
    if let selectedIndex = selectedPathIndex {
        let shape = shapes[selectedIndex]
        let bounds = shape.path.bounds
        let topRight = CGPoint(x: bounds.maxX, y: bounds.minY)
        let bottomRight = CGPoint(x: bounds.minX, y: bounds.maxY)
        let pointSize: CGFloat = 16
        
        // 判断点击区域是否在控制点范围内
        let topRightRect = CGRect(x: topRight.x - pointSize/2, y: topRight.y - pointSize/2, width: pointSize, height: pointSize)
        let bottomRightRect = CGRect(x: bottomRight.x - pointSize/2, y: bottomRight.y - pointSize/2, width: pointSize, height: pointSize)
        
        if topRightRect.contains(touchPoint) {
            draggingControlPoint = .topRight
            originalShape = shape
            return
        } else if bottomRightRect.contains(touchPoint) {
            draggingControlPoint = .bottomRight
            originalShape = shape
            return
        }
    }
    
    // 原图形选中逻辑,替换paths为shapes
    selectedPathIndex = nil
    for (index, shape) in shapes.enumerated() {
        if shape.path.bounds.contains(touchPoint) {
            selectedPathIndex = index
            originalShape = shape
            offset = CGPoint(x: touchPoint.x - shape.path.bounds.origin.x, y: touchPoint.y - shape.path.bounds.origin.y)
            setNeedsDisplay()
            return
        }
    }
    
    // 原绘制新图形逻辑
    path = UIBezierPath()
    if shapeType == .pencil {
        path.move(to: touchPoint)
    }
    startPoint = touchPoint
}

3. 修改touchesMoved逻辑

区分拖拽图形和拖拽控制点的处理逻辑:

override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
    guard let touch = touches.first else { return }
    let currentPoint = touch.location(in: self)
    
    // 处理控制点拖拽
    if let controlPoint = draggingControlPoint, let index = selectedPathIndex, let originShape = originalShape {
        var updatedPath: UIBezierPath?
        var updatedParams = originShape.parameters
        
        switch originShape.type {
        case .line:
            // 直线:控制点对应端点,拖拽时更新端点坐标
            switch controlPoint {
            case .topRight:
                updatedParams[1] = currentPoint
            case .bottomRight:
                updatedParams[0] = currentPoint
            }
            updatedPath = UIBezierPath()
            updatedPath?.move(to: updatedParams[0])
            updatedPath?.addLine(to: updatedParams[1])
            
        case .circle:
            // 圆形:拖拽控制点更新半径(保持正圆)
            let center = originShape.parameters[0]
            let newRadius = hypot(currentPoint.x - center.x, currentPoint.y - center.y)
            updatedPath = UIBezierPath(arcCenter: center, radius: newRadius, startAngle: 0, endAngle: 2 * .pi, clockwise: true)
            updatedParams[1] = currentPoint
            
        default:
            break
        }
        
        if let path = updatedPath {
            var newShape = originShape
            newShape.path = path
            newShape.parameters = updatedParams
            shapes[index] = newShape
            setNeedsDisplay()
        }
        return
    }
    
    // 处理图形拖拽移动逻辑,替换paths为shapes
    if let index = selectedPathIndex, let originShape = originalShape {
        let newOrigin = CGPoint(x: currentPoint.x - offset.x, y: currentPoint.y - offset.y)
        let translation = CGAffineTransform(translationX: newOrigin.x - originShape.path.bounds.origin.x, y: newOrigin.y - originShape.path.bounds.origin.y)
        
        var updatedShape = originShape
        updatedShape.path.apply(translation)
        // 同步平移图形参数
        updatedShape.parameters = updatedShape.parameters.map { $0.applying(translation) }
        shapes[index] = updatedShape
        setNeedsDisplay()
        return
    }
    
    // 原绘制新图形逻辑,替换paths为shapes
    guard let shapeType = shapeType, let startPoint = startPoint else { return }
    if shapeType == .pencil {
        path.addLine(to: currentPoint)
        setNeedsDisplay()
    } else {
        path.removeAllPoints()
        var params: [CGPoint] = []
        
        switch shapeType {
        case .line:
            path.move(to: startPoint)
            path.addLine(to: currentPoint)
            params = [startPoint, currentPoint]
        case .rectangle:
            path = UIBezierPath(rect: CGRect(origin: startPoint, size: CGSize(width: currentPoint.x - startPoint.x, height: currentPoint.y - startPoint.y)))
            params = [startPoint, currentPoint]
        case .circle:
            let radius = hypot(currentPoint.x - startPoint.x, currentPoint.y - startPoint.y)
            path = UIBezierPath(arcCenter: startPoint, radius: radius, startAngle: 0, endAngle: 2 * .pi, clockwise: true)
            params = [startPoint, currentPoint]
        case .pencil:
            break
        }
        setNeedsDisplay()
    }
}

4. 修改touchesEnded逻辑

保存新图形时使用ShapeData结构体:

override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
    guard let shapeType = shapeType else { return }
    if selectedPathIndex == nil && draggingControlPoint == nil {
        var params: [CGPoint] = []
        switch shapeType {
        case .line:
            guard let start = startPoint, let end = path.currentPoint else { return }
            params = [start, end]
        case .rectangle:
            guard let start = startPoint else { return }
            let end = CGPoint(x: start.x + path.bounds.width, y: start.y + path.bounds.height)
            params = [start, end]
        case .circle:
            guard let center = startPoint else { return }
            let radiusPoint = CGPoint(x: center.x + path.bounds.width/2, y: center.y)
            params = [center, radiusPoint]
        case .pencil:
            break
        }
        let newShape = ShapeData(type: shapeType, parameters: params, path: path.copy() as! UIBezierPath, color: strokeColor)
相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.22 00:10:03