Rezwan Khan chowdhury
Rezwan Khan chowdhury

Reputation: 495

Issue with Sequential Touch Points in Using CALayer for Drawing in Swift

I'm encountering an issue with my drawing functionality in Swift, specifically when using CALayer instead of UIBezierPath. Here's the problem I'm facing:

I am creating a texture from an image and using the texture to create a drawing line through finger stroke. In this process, the line is being created by joining the texture one by one, side by side. Initially, I used UIBezierPath for drawing, and everything worked fine. However, when I switched to using CALayer for drawing, for using textures to draw lines, I noticed that the touch points are not sequentially joining or increasing if I move my finger fast. drawing speed

When I move my finger slowly, the drawing works fine, and the touch points are joining sequentially. However, when I move my finger fast, the touch points seem to skip, resulting in disjointed lines and multiple blank spaces within a line.

class CustomStrokeDrawingView: UIView {
    var path = UIBezierPath()
    var startPoint = CGPoint()
    var touchPoint = CGPoint()
    var shape =  UIImage(named:"square-outline")

    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        let touch = touches.first
        if let point = touch?.location(in: self) {
            startPoint = point
        }
    }

    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
        let touch = touches.first
        if let point = touch?.location(in: self) {
            touchPoint = point
        }

        let imageLayer = CALayer()
        imageLayer.backgroundColor = UIColor.clear.cgColor
        imageLayer.bounds = CGRect(x: startPoint.x, y: startPoint.y , width: 20, height: 20)
        imageLayer.position = CGPoint(x:touchPoint.x ,y:touchPoint.y)
        imageLayer.contents = shape?.cgImage
        self.layer.addSublayer(imageLayer)
        
        
    }
    
    override func draw(_ rect: CGRect) {
        super.draw(rect)
    }
}

Upvotes: 0

Views: 70

Answers (1)

DonMag
DonMag

Reputation: 77672

It is very easy to move your finger faster than iOS can generate touchesMoved events ... if you log the points with print() statements, you can easily see this in the debug console. It's possible to "swipe" fast enough across the device screen and only generate 2 or 3 points.

One approach is to interpolate n-number of points on the lines between the touchesMoved points, and then draw your shape image at each of those points.

Here's some quick example code...


UIView subclass

class CustomStrokeDrawingView: UIView {
    
    var shape: UIImage!
    var pts: [CGPoint] = []

    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
    func commonInit() {
        // crash if we can't load the "brush" image
        guard let img =  UIImage(named:"square-outline") else {
            fatalError("Could not load shape image!")
        }
        shape = img
    }
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        let touch = touches.first
        if let startPoint = touch?.location(in: self) {
            // reset points array to only the starting point
            //print("Start:", startPoint)
            pts = [startPoint]
        }
    }
    
    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
        let touch = touches.first
        if let touchPoint = touch?.location(in: self) {
            //print("Moved:", touchPoint)
            pts.append(touchPoint)
            // trigger draw()
            setNeedsDisplay()
        }
    }

    override func draw(_ rect: CGRect) {
        super.draw(rect)
        
        guard pts.count > 1 else { return }

        let shapeSize: CGSize = shape.size
        
        // adjust as desired
        let pixelsPerIteration: CGFloat = 5.0

        for idx in 0..<(pts.count - 1) {
            
            let startx: CGFloat = pts[idx].x
            let starty: CGFloat = pts[idx].y
            let dx: CGFloat = pts[idx+1].x - startx
            let dy: CGFloat = pts[idx+1].y - starty
            
            let distance: CGFloat = sqrt(dx * dx + dy * dy)
            let iterations = distance / pixelsPerIteration + 1
            let dxIncrement = dx / iterations
            let dyIncrement = dy / iterations
            
            var dstRect: CGRect = .init(x: 0.0, y: 0.0, width: shapeSize.width, height: shapeSize.height)
            
            var x: CGFloat = startx - shapeSize.width / 2.0
            var y: CGFloat = starty - shapeSize.height / 2.0
            
            // draw a series of shape images to form the line
            for _ in 0..<Int(iterations) {
                dstRect.origin.x = x
                dstRect.origin.y = y
                x += dxIncrement
                y += dyIncrement
                shape.draw(in: dstRect)
            }
        }
    }
}

Example controller class

class DrawTestVC: UIViewController {
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        navigationController?.setNavigationBarHidden(true, animated: false)
        
        view.backgroundColor = .systemYellow
        
        let drawView = CustomStrokeDrawingView()
        drawView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(drawView)
        
        let g = view.safeAreaLayoutGuide
        NSLayoutConstraint.activate([
            drawView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
            drawView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
            drawView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
            drawView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -20.0),
        ])
        
        drawView.backgroundColor = UIColor(white: 0.95, alpha: 1.0)
    }
    
}

Looks about like this:

enter image description here

Upvotes: 1

Related Questions