swiftresizedrawingshapesuibezierpath

How to resize objects that have been drawn by UIBezierPath in swift iOS?


I have used UIBezierPath to draw objects like circle, line, freeline etc.... I have a functionality where i have to resize circle and line objects from the topRight and bottomRight variables in the "func drawPoints(bounds:CGRect)".

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
    }
}

I have achieved this: https://drive.google.com/file/d/1ZipL-fPNS2y-X-BWr5SgKYIRr1o0RXUg/view?usp=sharing

I want to achive something like this: https://drive.google.com/file/d/1aLAPbpjDzYPI5mCF2IQ00RJ02NU00YGc/view?usp=sharing

How do I achive this functionaltity? I have been seraching a solution for this from the past two days, but didn't find anything related to my functionaltity. Any kind of guidance would be really appreciated.

Thank you!


Solution

  • We can use CGAffineTransform to move and scale a path.

    Here's an example extension:

    extension UIBezierPath {
        func pathIn(targetRect: CGRect) -> UIBezierPath? {
            // get a copy of the path
            guard let newPath = copy() as? UIBezierPath else { return nil }
            
            // get current bounding rect
            let origRect = newPath.bounds
            
            // we want to translate (move) the path to 0,0
            let zeroTR = CGAffineTransform(translationX: -origRect.origin.x, y: -origRect.origin.y)
            newPath.apply(zeroTR)
            
            // now transform the path to new x,y and width,height
            let tr = CGAffineTransform(translationX: targetRect.origin.x, y: targetRect.origin.y)
                .scaledBy(x: targetRect.size.width / origRect.width, y: targetRect.size.height / origRect.height)
            newPath.apply(tr)
            
            return newPath
        }
    }
    

    This returns a copy of the original path, transformed to fit the target rect.

    So, if we have a path that has a bounding box of, say, (40.0, 40.0, 90.0, 60.0) and we want to scale it to a size of 180 x 120, we can do this:

    let targetRect: CGRect = .init(x: 40.0, y: 40.0, width: 180.0, height: 120.0)
    let transformedPath = originalPath.pathIn(targetRect: targetRect)
    

    Example:

    example anim

    Here's some example code to create that...

    Drawing view - draws a Blue path with dashed bounding box in its original state, and a Red transformed path (with dashed bounding box) in targetRect

    class SampleDrawView: UIView {
        
        public var currentPath: UIBezierPath! { didSet { setNeedsDisplay() } }
        public var targetRect: CGRect = .zero { didSet { setNeedsDisplay() } }
        
        override func draw(_ rect: CGRect) {
            
            // don't try to draw if we don't have a path
            guard currentPath != nil else { return }
            
            // let's draw a dashed-outline rect, 2-points larger than the original path's bounding box
            UIColor.systemBlue.setStroke()
            let boxPath = UIBezierPath(rect: currentPath.bounds.insetBy(dx: -2.0, dy: -2.0))
            boxPath.setLineDash([8, 8], count: 1, phase: 0.0)
            boxPath.stroke()
            
            // draw the original path in blue
            UIColor.blue.setStroke()
            currentPath.lineWidth = 3
            currentPath.stroke()
            
            // make sure targetRect has been set
            guard targetRect != .zero else { return }
            
            // make sure we get a valid transformed path that fits in the target rectangle
            guard let transformedPath = currentPath.pathIn(targetRect: targetRect) else { return }
            
            // let's draw a dashed-outline rect, 2-points larger than the target rect
            UIColor.lightGray.setStroke()
            let targetBoxPath = UIBezierPath(rect: targetRect.insetBy(dx: -2.0, dy: -2.0))
            targetBoxPath.setLineDash([8, 8], count: 1, phase: 0.0)
            targetBoxPath.stroke()
    
            // now we draw the transformed path in red
            UIColor.red.setStroke()
            transformedPath.lineWidth = 3
            transformedPath.stroke()
            
        }
    
    }
    

    Sample controller - tap anywhere to cycle through various target rectangles

    class SamplePathVC: UIViewController {
        
        var samplePath: UIBezierPath!
        var someRects: [CGRect] = []
        
        let exampleView = SampleDrawView()
        
        override func viewDidLoad() {
            super.viewDidLoad()
            view.backgroundColor = .systemBackground
            
            exampleView.backgroundColor = UIColor(white: 0.975, alpha: 1.0)
            exampleView.translatesAutoresizingMaskIntoConstraints = false
            view.addSubview(exampleView)
            
            let g = view.safeAreaLayoutGuide
            NSLayoutConstraint.activate([
                exampleView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
                exampleView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
                exampleView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
                exampleView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -20.0),
            ])
    
            samplePath = UIBezierPath()
            samplePath.move(to: .init(x: 40.0, y: 40.0))
            samplePath.addLine(to: .init(x: 100.0, y: 40.0))
            samplePath.addQuadCurve(to: .init(x: 100.0, y: 100.0), controlPoint: .init(x: 160.0, y: 70.0))
            samplePath.addLine(to: .init(x: 80.0, y: 80.0))
            samplePath.addLine(to: .init(x: 60.0, y: 100.0))
            samplePath.addLine(to: .init(x: 40.0, y: 80.0))
    
            exampleView.currentPath = samplePath
            
            let x: CGFloat = samplePath.bounds.origin.x
            let y: CGFloat = samplePath.bounds.origin.y
            let w: CGFloat = samplePath.bounds.size.width
            let h: CGFloat = samplePath.bounds.size.height
    
            someRects = [
                // scale by 2x
                .init(x: x, y: y, width: w * 2.0, height: h * 2.0),
                // scale by 3x
                .init(x: x, y: y, width: w * 3.0, height: h * 3.0),
                // move but no scale
                .init(x: x + 100.0, y: y + 100.0, width: w, height: h),
                // the rest are move AND scale
                .init(x:  40.0, y: 120.0, width: 120.0, height: 120.0),
                .init(x:  40.0, y: 120.0, width: 240.0, height: 240.0),
                .init(x:  40.0, y: 120.0, width: 240.0, height: 500.0),
                .init(x: 200.0, y:  60.0, width:  60.0, height: 560.0),
                .init(x:  20.0, y: 300.0, width: 280.0, height:  80.0),
            ]
            
        }
        
        override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
            // cycle through the sample target rects on each tap
            let r = someRects.removeFirst()
            someRects.append(r)
            exampleView.targetRect = r
        }
        
    }