I am using SwiftyDraw library to draw a path for my app. Drawing the path is working well but there is a problem if user start drawing path and move the finger back and forth in the same position. Still its drawing the path and it gets completed. Here I have my whole code.
lazy var targetView: UIView = {
let sTargetView = UIView(frame: CGRect(x: 0,
y: 0,
width: self.view.frame.width,
height: self.view.frame.height/1.3))
sTargetView.center = CGPoint(x: UIScreen.main.bounds.size.width/2,
y: UIScreen.main.bounds.size.height/2)
sTargetView.backgroundColor = .clear
sTargetView.isUserInteractionEnabled = true
canvasView.isUserInteractionEnabled = true
canvasView.backgroundColor = .clear
sTargetView.addSubview(canvasView)
canvasView.brush = .thick
canvasView.brush.color = self.assistiveTouchSwitch.isOn ? Color(.clear) : Color.init(self.accentColor)
canvasView.frame = sTargetView.bounds
canvasView.delegate = self
return sTargetView
}()
var swiped = false
var lastPoint = CGPoint.zero
var accentColor = UIColor.red//UIColor(hex: "00acc1")
let canvasView = SwiftyDrawView()
var isTutorialAnimationInProgresss = false
var strokeAnimationCounter = 0
var currentPathToTrace : CGPath!
var defaultTransform = CGAffineTransform()
var assistiveDrawLayersArray = [CAShapeLayer()]
var pathToHitTestAgainst : CGPath!
let dashLayer = CAShapeLayer()
let tutorialLayer = CALayer()
var strokePathsArray = [MyBezierPath]()
var strokeIndex = 0 {
didSet {
self.pathToHitTestAgainst = self.strokePathsArray[strokeIndex].cgPath.copy(strokingWithWidth: 10, lineCap: .round, lineJoin: .round, miterLimit: 0, transform: defaultTransform)
}
}
@IBOutlet weak var assistiveTouchSwitch: UISwitch!
override func viewDidload() {
self.view.addSubview(targetView)
let p1 = MyBezierPath(svgPath: "M 55 20 V 90")
let p2 = MyBezierPath(svgPath: "M 55 47 C 10 18 11 90 55 63")
let p3 = MyBezierPath(svgPath: "M 55 47 C 93 25 94 62 71 73")
let p4 = MyBezierPath(svgPath: "M 35 20 H 80")
strokePathsArray.append(p1)
strokePathsArray.append(p2)
strokePathsArray.append(p3)
strokePathsArray.append(p4)
let combinedPath = CGMutablePath()
combinedPath.addPath(p1.cgPath)
combinedPath.addPath(p2.cgPath)
combinedPath.addPath(p3.cgPath)
combinedPath.addPath(p4.cgPath)
defaultTransform = CGAffineTransform(scaleX: self.targetView.frame.width/109, y: self.targetView.frame.width/109)
let shapeLayer = CAShapeLayer()
shapeLayer.backgroundColor = UIColor.cyan.cgColor
shapeLayer.transform = CATransform3DMakeAffineTransform(defaultTransform)
// The Bezier path that we made needs to be converted to
// a CGPath before it can be used on a layer.
shapeLayer.path = combinedPath
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.lineWidth = 10
shapeLayer.lineCap = .round
shapeLayer.strokeColor = UIColor.white.cgColor
self.targetView.layer.addSublayer(shapeLayer)
self.pathToHitTestAgainst = self.strokePathsArray[0].cgPath.copy(strokingWithWidth: 10, lineCap: .round, lineJoin: .round, miterLimit: 0, transform: defaultTransform)
animatePath()
setUpLayersForAssistiveMode(color: accentColor)
self.targetView.layer.addSublayer(self.canvasView.layer)
}
func setUpLayersForAssistiveMode(color: UIColor){
self.assistiveDrawLayersArray.removeAll()
if self.assistiveTouchSwitch.isOn {
self.strokePathsArray.forEach{
let layer = CAShapeLayer()
layer.path = $0.cgPath
layer.fillColor = UIColor.clear.cgColor
layer.lineWidth = 6
layer.strokeStart = 0
layer.strokeEnd = 0
layer.lineCap = .round
layer.strokeColor = color.cgColor
layer.transform = CATransform3DMakeAffineTransform(defaultTransform)
self.targetView.layer.addSublayer(layer)
assistiveDrawLayersArray.append(layer)
}
}
}
func showHint(){
let path = self.strokePathsArray[strokeIndex]
dashLayer.path = path.cgPath
dashLayer.fillColor = UIColor.clear.cgColor
dashLayer.lineDashPattern = [2,2]
dashLayer.contents = UIImage(named: "pen")?.cgImage
dashLayer.transform = CATransform3DMakeAffineTransform(defaultTransform)
dashLayer.strokeColor = UIColor(hex: "f06292").cgColor
dashLayer.lineWidth = 0.5
self.currentPathToTrace = path.cgPath.copy(strokingWithWidth: 0, lineCap: .round, lineJoin: .miter, miterLimit: 0, transform: defaultTransform)
if let startPoint = path.startPoint {
let circulPath = UIBezierPath(arcCenter: CGPoint(x: startPoint.x , y: startPoint.y) , radius: 1.5, startAngle: 0, endAngle: 2.0 * CGFloat.pi, clockwise: true)
let circleLayer = CAShapeLayer()
circleLayer.path = circulPath.cgPath
circleLayer.fillColor = UIColor(hex: "f06292").cgColor
circleLayer.transform = CATransform3DMakeTranslation(0, 0, 0)
dashLayer.insertSublayer(circleLayer, at: 0)
}
let secondLastPoint = path.cgPath.points().count>2 ? path.cgPath.points()[path.cgPath.points().count-2] : path.cgPath.points()[0]
if let lastPoint = path.cgPath.points().last {
let angle = atan2((lastPoint.y - secondLastPoint.y), (lastPoint.x - secondLastPoint.x))
let distance: CGFloat = 1.0
let path = UIBezierPath()
path.move(to: lastPoint)
path.addLine(to: calculatePoint(from: lastPoint, angle: angle + CGFloat.pi/2, distance: distance)) // to the right
path.addLine(to: calculatePoint(from: lastPoint, angle: angle, distance: distance)) // straight ahead
path.addLine(to: calculatePoint(from: lastPoint, angle: angle - CGFloat.pi/2, distance: distance)) // to the left
path.close()
let arrowHeadLayer = CAShapeLayer()
arrowHeadLayer.path = path.cgPath
arrowHeadLayer.lineWidth = 1
arrowHeadLayer.strokeColor = UIColor(hex: "f06292").cgColor
arrowHeadLayer.fillColor = UIColor.white.cgColor
dashLayer.insertSublayer(arrowHeadLayer, at: 1)
}
}
func showTutorial(){
if isTutorialAnimationInProgresss {return} //avoid animation on repeated taps outside boundary
let path = self.strokePathsArray[strokeIndex]
self.tutorialLayer.opacity = 1
tutorialLayer.contents = UIImage(named: "pen")?.cgImage
tutorialLayer.frame = CGRect(x: 0, y: 0, width: 20, height: 20)
tutorialLayer.anchorPoint = CGPoint.zero
let animation = CAKeyframeAnimation(keyPath: #keyPath(CALayer.position))
animation.duration = 1
animation.repeatCount = 0 // just recommended by Apple
animation.path = path.cgPath
animation.calculationMode = .paced
animation.beginTime = 0
animation.fillMode = .forwards
animation.isRemovedOnCompletion = false
CATransaction.begin()
isTutorialAnimationInProgresss = true
CATransaction.setCompletionBlock({
self.tutorialLayer.opacity = 0
self.isTutorialAnimationInProgresss = false
})
tutorialLayer.add(animation, forKey: nil)
CATransaction.commit()
dashLayer.addSublayer(tutorialLayer)
self.targetView.layer.addSublayer(dashLayer)
}
func calculatePoint(from point: CGPoint, angle: CGFloat, distance: CGFloat) -> CGPoint {
return CGPoint(x: point.x + CGFloat(cosf(Float(angle))) * distance, y: point.y + CGFloat(sinf(Float(angle))) * distance)
}
func animatePath() {
if strokeAnimationCounter == self.strokePathsArray.count { return }
let layer: CAShapeLayer = CAShapeLayer()
layer.strokeColor = self.accentColor.cgColor
layer.lineWidth = 5.0
layer.fillColor = UIColor.clear.cgColor
layer.borderWidth = 0
layer.lineCap = .round
layer.borderColor = UIColor.clear.cgColor
layer.transform = CATransform3DMakeAffineTransform(defaultTransform)
layer.path = self.strokePathsArray[strokeAnimationCounter].cgPath
CATransaction.begin()
let animation: CABasicAnimation = CABasicAnimation(keyPath: "strokeEnd")
animation.fromValue = 0.0
animation.toValue = 1.0
animation.duration = 1.0
CATransaction.setCompletionBlock { [weak self] in
guard let strongSelf = self else {
print("ViewController was deallocated, stopping animation")
return
}
strongSelf.strokeAnimationCounter += 1
if strongSelf.strokeAnimationCounter <= strongSelf.strokePathsArray.count - 1 {
strongSelf.animatePath()
} else {
strongSelf.canvasView.layer.sublayers?
.filter { $0 is CAShapeLayer }
.forEach { $0.removeFromSuperlayer() }
strongSelf.showHint()
strongSelf.showTutorial()
}
print("Animation completed")
}
layer.add(animation, forKey: "myStroke")
CATransaction.commit()
self.canvasView.layer.addSublayer(layer)
}
class MyBezierPath: UIBezierPath {
var startPoint :CGPoint?
override func move(to point: CGPoint) {
super.move(to: point)
startPoint=point
}
}
extension CGPath {
func points() -> [CGPoint]{
var bezierPoints = [CGPoint]()
self.forEach(body: { (element: CGPathElement) in
let numberOfPoints: Int = {
switch element.type {
case .moveToPoint, .addLineToPoint: // contains 1 point
return 1
case .addQuadCurveToPoint: // contains 2 points
return 2
case .addCurveToPoint: // contains 3 points
return 3
case .closeSubpath:
return 0
}
}()
for index in 0..<numberOfPoints {
let point = element.points[index]
bezierPoints.append(point)
}
})
return bezierPoints
}
func forEach( body:@escaping @convention(block) (CGPathElement) -> Void) {
typealias Body = @convention(block) (CGPathElement) -> Void
func callback(info: UnsafeMutableRawPointer?, element: UnsafePointer<CGPathElement>) {
let body = unsafeBitCast(info, to: Body.self)
body(element.pointee)
}
let unsafeBody = unsafeBitCast(body, to: UnsafeMutableRawPointer.self)
self.apply(info: unsafeBody, function: callback)
}}
extension DrawVC: SwiftyDrawViewDelegate{
func swiftyDraw(didCancelDrawingIn drawingView: SwiftyDrawView, using touch: UITouch) {}
func swiftyDraw(shouldBeginDrawingIn drawingView: SwiftyDrawView, using touch: UITouch) -> Bool {
return true
}
func swiftyDraw(didBeginDrawingIn drawingView: SwiftyDrawView, using touch: UITouch) {}
func swiftyDraw(isDrawingIn drawingView: SwiftyDrawView, using touch: UITouch) {
let point = touch.location(in: drawingView)
let iscontain = pathToHitTestAgainst.contains(point)
if iscontain {
// print("contains")
if self.assistiveTouchSwitch.isOn {
if let first = self.currentPathToTrace.points().first{
print("distamce:", first.distance(to: point))
print("length: ", currentPathToTrace.length/2)
if first.distance(to: point)>=21 && assistiveDrawLayersArray[strokeIndex].strokeEnd == 0 {
print("from right")
assistiveDrawLayersArray[strokeIndex].strokeEnd = 0
showTutorial()
return
}
}
if let drawItem = drawingView.drawItems.last {
let offset = drawItem.path.length/(self.currentPathToTrace.length/2)
// print(offset)
if offset >= 0.9{
CATransaction.begin()
CATransaction.setDisableActions(false)
assistiveDrawLayersArray[strokeIndex].strokeEnd = 1
CATransaction.commit()
if strokeIndex == strokePathsArray.count-1{
dashLayer.removeFromSuperlayer()
dashLayer.sublayers?.filter{ $0 is CAShapeLayer }.forEach{ $0.removeFromSuperlayer() }
drawingView.clear()
return
}
dashLayer.sublayers?.filter{ $0 is CAShapeLayer }.forEach{ $0.removeFromSuperlayer() }
drawingView.clear()
self.strokeIndex+=1
showHint()
showTutorial()
}else{
CATransaction.begin()
CATransaction.setDisableActions(true)
assistiveDrawLayersArray[strokeIndex].strokeEnd = offset
CATransaction.commit()
print("hello")
}
}
return
}
}else{
// drawingView.undo()
if self.assistiveTouchSwitch.isOn {
if let drawItem = drawingView.drawItems.last {
let offset = drawItem.path.length/(self.currentPathToTrace.length/2)
let progress = Int(max(0, offset))
// print (offset)
if progress != 1{
assistiveDrawLayersArray[strokeIndex].strokeEnd = 0
drawingView.clear()
showTutorial()
}
}
return
}
drawingView.undo()
showTutorial()
}
// print(drawingView.currentPoint)
}
func swiftyDraw(didFinishDrawingIn drawingView: SwiftyDrawView, using touch: UITouch) {
if self.assistiveTouchSwitch.isOn {
if let drawItem = drawingView.drawItems.last {
let offset = drawItem.path.length/(self.currentPathToTrace.length/2)
let progress = Int(max(0, offset))
// print("progress :", offset)
if progress == 1{
self.assistiveDrawLayersArray[strokeIndex].strokeEnd = 1
if strokeIndex == strokePathsArray.count-1{
dashLayer.removeFromSuperlayer()
dashLayer.sublayers?.filter{ $0 is CAShapeLayer }.forEach{ $0.removeFromSuperlayer() }
drawingView.clear()
return
}
dashLayer.sublayers?.filter{ $0 is CAShapeLayer }.forEach{ $0.removeFromSuperlayer() }
drawingView.clear()
self.strokeIndex+=1
showHint()
showTutorial()
}else{
// print("progress : \(progress)")
drawingView.clear()
self.showTutorial()
assistiveDrawLayersArray[strokeIndex].strokeEnd = 0
}
}
return
}else{
// print(MyBezierPath(cgPath: strokePath!).length/2)
if let drawItem = drawingView.drawItems.last{
print(currentPathToTrace.length/2 - drawItem.path.length)
if abs(currentPathToTrace.length/2 - drawItem.path.length) >= 17{
drawingView.undo()
showTutorial()
} else {
let layer = CAShapeLayer()
layer.fillColor = UIColor.clear.cgColor
layer.lineWidth = 10
layer.lineCap = .round
layer.strokeColor = self.accentColor.cgColor
layer.path = drawItem.path
self.canvasView.layer.addSublayer(layer)
drawingView.clear()
dashLayer.sublayers?.filter{ $0 is CAShapeLayer }.forEach{ $0.removeFromSuperlayer() }
if strokeIndex == strokePathsArray.count-1{
dashLayer.removeFromSuperlayer()
return
}
self.strokeIndex+=1
showHint()
showTutorial()
}
}
}
}}
Above code is almost whole code to draw a path. In func swiftyDraw(isDrawingIn drawingView: SwiftyDrawView, using touch: UITouch) this method in case of self.assistiveTouchSwitch.isOn = true condition I want to stop user from back tracing the already drawn path. I tried to check offset to prevent the thing but offset is never decreasing its value even if user draw backward.
I request any genius to resolve this issue for me. Thanks a lot.
There are problems with comparing the length of the "path to trace" with the length of the user's path.
If the user does not do a smooth drag:
Or, worse, if the user "back-tracks" on the path:
The user's path can be considerable longer than the trace path.
When you implement your "assisted drawing" it becomes obvious:
So, instead of comparing path lengths, we can generate an array of "points along the path" and track how far into that array the user gets.
You already have a function that returns a CGPoint
at a percentage of the path, so we can do this:
let path = MyBezierPath(svgPath: "m 17.899207,12.838052 c 24.277086,0 48.554171,0 72.831257,0")
let cpth = path.cgPath.copy(using: &defaultTransform)!
numPointsOnPath = 10
var pointsAlongPath: [CGPoint] = []
for i in 0...numPointsOnPath {
let pct = CGFloat(i) / CGFloat(numPointsOnPath)
guard let p = cpth.point(at: pct) else {
fatalError("could not get point at: \(i) / \(pct)")
}
pointsAlongPath.append(p)
}
then we can use this function to find the index into that array of the closest point to the user's current drag point:
// find the CGPoint in array of CGPoint, closest to target CGPoint
func findClosestPointIndex(to target: CGPoint, in points: [CGPoint]) -> Int? {
guard !points.isEmpty else { return nil }
return points.enumerated().min(by: { $0.element.distance(to: target) < $1.element.distance(to: target) })?.offset
}
and, as the user drags to trace the path:
if let pIDX = findClosestPointIndex(to: p, in: pointsAlongPath[tracingIDX]) {
assitedLayer.strokeEnd = CGFloat(pIDX) / CGFloat(numPointsOnPath)
}
If we don't want to decrease the path if/when the user back-traces, we can track a "maxIDX":
To avoid the "jumping" we can generate 100 points, instead of 10 (for this explanation, 10 was easier to see what was going on):
Putting it all together:
and the same, but with 100 points:
That should get you on your way. If you have trouble implementing that approach, feel free to ask for more help.
Edit - addressing comments...
Don't use a "stroked" version of the path to trace.
Generate the array of points once, not every time the touch moves.
func showHint(){
let path = self.strokePathsArray[strokeIndex]
dashLayer.path = path.cgPath
dashLayer.fillColor = UIColor.clear.cgColor
dashLayer.lineDashPattern = [2,2]
dashLayer .contents = UIImage(named: "pen")?.cgImage
dashLayer.transform = CATransform3DMakeAffineTransform(defaultTransform)
dashLayer.strokeColor = UIColor(hex: "f06292").cgColor
dashLayer.lineWidth = 0.5
// use the path itself, not a stroked version
//self.currentPathToTrace = path.cgPath.copy(strokingWithWidth: 0, lineCap: .round, lineJoin: .miter, miterLimit: 0, transform: defaultTransform)
self.currentPathToTrace = path.cgPath.copy(using: &defaultTransform)
// generate array of points along path here,
// not in swiftyDraw(isDrawingIn ...)
pointsAlongPath = []
for i in 0...numPointsOnPath {
let pct = CGFloat(i) / CGFloat(numPointsOnPath)
guard let p = currentPathToTrace.point(at: pct) else {
fatalError("could not get point at: \(i) / \(pct)")
}
pointsAlongPath.append(p)
}
if let startPoint = path.startPoint {
... the rest of your code in this func