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