I'm drawing lines with UIBezierPath in draw(_ rect: CGRect) method previously i did tried that with CAShapeLayer but i wasn't able to select particular path and move it so i'm trying this, but after drawing i'm not able to access that BezierPath from view's subviews or it's sublayers and not directly from UIView. How can i access that bezierpath drawn in drawRect method so i can change it's position according to touches.
private var bezierPaths: [MyBezierPath] = [MyBezierPath]()
override func draw(_ rect: CGRect) {
super.draw(rect)
UIColor.orange.set()
for path in bezierPaths {
path.lineWidth = 4
path.lineCapStyle = .round
path.lineJoinStyle = .round
path.stroke()
}
}
func drawingPath(_ path: [MyBezierPath]) {
bezierPaths = path
}
like creating CAShapeLayer and setting layer.path = BezierPath like this :
let shapeLayer = CAShapeLayer()
shapeLayer.lineWidth = 1
shapeLayer.strokeColor = UIColor.black.cgColor
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.lineCap = .round
shapeLayer.lineJoin = .round
shapeLayer.lineDashPattern = [10, 10]
shapeLayer.name = "ShapeLayer"
self.canvas.layer.addSublayer(shapeLayer)
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
if canvas.layer.sublayers != nil && canvas.layer.sublayers?.last?.name == "ShapeLayer" {
guard let layer = canvas.layer.sublayers?.last as? CAShapeLayer else { return }
layer.path = path.cgPath
}
}
like this i can access layer.path but how can i do it for path drawn in draw(Rect:) method ?
Depending on what you're actually trying to do, overriding draw(_:)
may not be the best approach.
For example, if you want to animate the drawn paths, it will be much easier if you are using CAShapeLayer
as sublayers, or using subviews.
Also, shape layers are highly optimized ... if you write your own draw / animate functions you risk ending up with lesser-optimized code.
However, whichever approach you take, to "find the path" you want to use contains(_ point: CGPoint)
For example, if you have an array of UIBezierPath
, with all paths relative to the top-left (0,0
) of the view, on touch you could do:
// find a path that contains the touch point
if let touchedPath = bezierPaths.first(where: {$0.contains(point)}) {
// do something with that path
}
If the paths are not relative to the top-left of the view - for example, if the path is relative to the layer position, or is part of a subview - you'd need to convert the touch point.
Here's a quick example that looks like this - on load, and then after dragging a few shapes around:
We'll start with a simple "path" struct, which contains the bezier path and the color to use. We could add various other properties, such as line width, dash pattern, etc:
struct MyPath {
var color: UIColor = .white
var path: UIBezierPath = UIBezierPath()
}
then we'll use this UIView
subclass that will handle the drawing, as well as touches began/moved/ended:
class BezDrawView: UIView {
var myPaths: [MyPath] = [] {
didSet {
setNeedsDisplay()
}
}
// used to track path to move
var activePath: MyPath?
var startPoint: CGPoint = .zero
override func draw(_ rect: CGRect) {
super.draw(rect)
if myPaths.count > 0 {
myPaths.forEach { p in
p.color.set()
p.path.stroke()
}
}
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let t = touches.first else { return }
let point = t.location(in: self)
// find a path that contains the touch point
if let touchedPath = myPaths.first(where: {$0.path.contains(point)}) {
self.activePath = touchedPath
self.startPoint = point
return
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let ap = activePath, let t = touches.first else { return }
let point = t.location(in: self)
// move the path by the distance the touch moved
let tr = CGAffineTransform(translationX: point.x - startPoint.x, y: point.y - startPoint.y)
ap.path.apply(tr)
startPoint = point
// this triggers draw(_:)
setNeedsDisplay()
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
// done dragging
activePath = nil
}
}
and an example controller which defines some sample shapes (paths):
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
let testBezDrawView = BezDrawView()
testBezDrawView.backgroundColor = UIColor(white: 0.1, alpha: 1.0)
testBezDrawView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(testBezDrawView)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// constrain bez draw view to all 4 sides with 20-points "padding"
testBezDrawView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
testBezDrawView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
testBezDrawView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
testBezDrawView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -20.0),
])
// add some sample path shapes and colors
let colors: [UIColor] = [
.systemRed, .systemGreen, .systemBlue,
.cyan, .magenta, .yellow, .orange, .green,
]
let rects: [CGRect] = [
CGRect(x: 20, y: 20, width: 60, height: 60),
CGRect(x: 180, y: 20, width: 40, height: 60),
CGRect(x: 20, y: 120, width: 60, height: 100),
CGRect(x: 200, y: 140, width: 50, height: 90),
CGRect(x: 90, y: 220, width: 100, height: 60),
CGRect(x: 220, y: 260, width: 80, height: 160),
CGRect(x: 50, y: 360, width: 200, height: 100),
CGRect(x: 150, y: 480, width: 120, height: 80),
]
var somePaths: [MyPath] = []
var i: Int = 0
for (c, r) in zip(colors, rects) {
var b = UIBezierPath()
switch i % 4 {
case 1: // oval
b = UIBezierPath(ovalIn: r)
case 2: // triangle shape
b = UIBezierPath()
b.move(to: CGPoint(x: r.minX, y: r.maxY))
b.addLine(to: CGPoint(x: r.midX, y: r.minY))
b.addLine(to: CGPoint(x: r.maxX, y: r.maxY))
b.close()
case 3: // diamond
b = UIBezierPath()
b.move(to: CGPoint(x: r.minX, y: r.midY))
b.addLine(to: CGPoint(x: r.midX, y: r.minY))
b.addLine(to: CGPoint(x: r.maxX, y: r.midY))
b.addLine(to: CGPoint(x: r.midX, y: r.maxY))
b.close()
default: // rect
b = UIBezierPath(rect: r)
}
b.lineWidth = 4
b.lineCapStyle = .round
b.lineJoinStyle = .round
b.setLineDash([5, 10], count: 2, phase: 0)
let p = MyPath(color: c, path: b)
somePaths.append(p)
i += 1
}
testBezDrawView.myPaths = somePaths
}
}
Here's a video of it in use (too big to convert to gif and embed here):
https://i.sstatic.net/66awT.jpg
Edit in response to comment...
That needs very few changes to make it work with shape layers instead of draw(_:)
.
We can use the same Translation Transform to "move" the path, then update the .path
property of that path's associated layer:
let tr = CGAffineTransform(translationX: point.x - startPoint.x, y: point.y - startPoint.y)
active.path.apply(tr)
activeLayer.path = activeParh.path.cgPath
I strongly, strongly recommend that you try to convert that sample code to shape layers on your own - it would be a good learning exercise.
But, if you run into trouble, here's a modified version...
First, we're going to use the. array index of the MyPath
object to match with the sublayer index, so we need to make our struct Equatable
:
struct MyPath: Equatable {
var color: UIColor = .white
var path: UIBezierPath = UIBezierPath()
}
Then some minor changes to BezDrawView
-- which we'll name BezLayerView
:
class BezLayerView: UIView {
var myPaths: [MyPath] = [] {
didSet {
// remove any existing layers
if let subs = layer.sublayers {
subs.forEach { lay in
lay.removeFromSuperlayer()
}
}
// create layers for paths
myPaths.forEach { p in
let lay = CAShapeLayer()
lay.lineWidth = 4
lay.lineCap = .round
lay.lineJoin = .round
lay.lineDashPattern = [5, 10]
lay.fillColor = UIColor.clear.cgColor
lay.strokeColor = p.color.cgColor
lay.path = p.path.cgPath
layer.addSublayer(lay)
}
}
}
// used to track path to move
var activeLayer: CAShapeLayer?
var activePath: MyPath?
var startPoint: CGPoint = .zero
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let t = touches.first, let subs = layer.sublayers else { return }
let point = t.location(in: self)
// find a path that contains the touch point
if let touchedPath = myPaths.first(where: {$0.path.contains(point)}) {
// find the layer associated with that path
if let idx = myPaths.firstIndex(of: touchedPath) {
if let lay = subs[idx] as? CAShapeLayer {
self.activePath = touchedPath
self.activeLayer = lay
self.startPoint = point
}
}
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let lay = activeLayer, let ap = activePath, let t = touches.first else { return }
let point = t.location(in: self)
// move the path by the distance the touch moved
let tr = CGAffineTransform(translationX: point.x - startPoint.x, y: point.y - startPoint.y)
ap.path.apply(tr)
lay.path = ap.path.cgPath
startPoint = point
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
// done dragging
activeLayer = nil
activePath = nil
}
}
and an almost identical version of the controller:
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
let testBezLayerView = BezLayerView()
testBezLayerView.backgroundColor = UIColor(white: 0.1, alpha: 1.0)
testBezLayerView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(testBezLayerView)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// constrain bez draw view to all 4 sides with 20-points "padding"
testBezLayerView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
testBezLayerView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
testBezLayerView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
testBezLayerView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -20.0),
])
// add some sample path shapes and colors
let colors: [UIColor] = [
.systemRed, .systemGreen, .systemBlue,
.cyan, .magenta, .yellow, .orange, .green,
]
let rects: [CGRect] = [
CGRect(x: 20, y: 20, width: 60, height: 60),
CGRect(x: 180, y: 20, width: 40, height: 60),
CGRect(x: 20, y: 120, width: 60, height: 100),
CGRect(x: 200, y: 140, width: 50, height: 90),
CGRect(x: 90, y: 220, width: 100, height: 60),
CGRect(x: 220, y: 260, width: 80, height: 160),
CGRect(x: 50, y: 360, width: 200, height: 100),
CGRect(x: 150, y: 480, width: 120, height: 80),
]
var somePaths: [MyPath] = []
var i: Int = 0
for (c, r) in zip(colors, rects) {
var b = UIBezierPath()
switch i % 4 {
case 1: // oval
b = UIBezierPath(ovalIn: r)
case 2: // triangle shape
b = UIBezierPath()
b.move(to: CGPoint(x: r.minX, y: r.maxY))
b.addLine(to: CGPoint(x: r.midX, y: r.minY))
b.addLine(to: CGPoint(x: r.maxX, y: r.maxY))
b.close()
case 3: // diamond
b = UIBezierPath()
b.move(to: CGPoint(x: r.minX, y: r.midY))
b.addLine(to: CGPoint(x: r.midX, y: r.minY))
b.addLine(to: CGPoint(x: r.maxX, y: r.midY))
b.addLine(to: CGPoint(x: r.midX, y: r.maxY))
b.close()
default: // rect
b = UIBezierPath(rect: r)
}
let p = MyPath(color: c, path: b)
somePaths.append(p)
i += 1
}
testBezLayerView.myPaths = somePaths
}
}
The output and functionality should be indistinguishable from the BezDrawView
implementation.