iosswiftdraw

Prevent backward tracing in iOS Drawing App (Swift)


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.


Solution

  • 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:

    step 1

    Or, worse, if the user "back-tracks" on the path:

    step 1

    The user's path can be considerable longer than the trace path.

    When you implement your "assisted drawing" it becomes obvious:

    step 1

    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":

    step 1

    To avoid the "jumping" we can generate 100 points, instead of 10 (for this explanation, 10 was easier to see what was going on):

    step 1

    Putting it all together:

    step 1

    and the same, but with 100 points:

    step 1

    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