How to block touchesMoved events if there is a view above the receiving view

62 Views Asked by At

I am developing a paint feature with a main canvas view and a floating toolbar above it. In my CanvasView I have:

final class CanvasView: UIView {

    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        guard let touch = touches.first else { return }

        let currentPoint = touch.location(in: self)
        print("touch began \(currentPoint)")
    }

    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
        guard let touch = touches.first else { return }

        let currentPoint = touch.location(in: self)

        print("touch move \(currentPoint)")
    }

}

I add this to a view controller along with a toolbar:

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        let canvasView = CanvasView()
        canvasView.backgroundColor = .white
        view.addSubview(canvasView)

        canvasView.snp.makeConstraints { make in
            make.edges.equalToSuperview()
        }

        let toolbarView = UIView()
        toolbarView.backgroundColor = .black.withAlphaComponent(0.7)
        view.addSubview(toolbarView)

        toolbarView.snp.makeConstraints { make in
            make.leading.trailing.bottom.equalToSuperview()
            make.height.equalTo(200.0)
        }
    }

}

This looks like the following:

enter image description here

When I start dragging my finger around the canvas the touchesBegan and touchesMoved events are logged as follows. However if I move into the toolbar region touchesMoved is still logged. Note that I cannot trigger touchesBegan here.

I guess it's logical that touchesBegan won't fire since the canvas is obscured by the toolbar.

How would I go about stopping touchesMoved being triggered when dragging from the canvas into the toolbar area?

1

There are 1 best solutions below

0
DonMag On

One option...

Use a closure so your CanvasView can get the frame of the toolbarView and not execute any code if the touch point is inside that frame.

So, change your CanvasView to this:

final class CanvasView: UIView {
    
    // closure to get the tool bar view frame
    var getToolbarFrame: (() ->(CGRect))?
    
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        guard let touch = touches.first else { return }
        
        let currentPoint = touch.location(in: self)
        print("touch began \(currentPoint)")
    }
    
    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
        guard let touch = touches.first else { return }
        
        let currentPoint = touch.location(in: self)
        
        // unwrap the optional closure to get the toolbar view frame
        if let r = getToolbarFrame?() {
            // convert that rect to self's coordinate space
            let rr = self.superview!.convert(r, to: self)
            //  if the frame contains this touch point,
            //  return
            if rr.contains(currentPoint) {
                return
            }
        }
        
        // toobar view frame did not contain the touch, so
        //  do what you want with the moved touch
        print("touch move \(currentPoint)")
    }

}

and set the closure inside viewDidLoad in your controller:

class ViewController: UIViewController {
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let canvasView = CanvasView()
        canvasView.backgroundColor = .white
        view.addSubview(canvasView)
        
        canvasView.snp.makeConstraints { make in
            make.edges.equalToSuperview()
        }
        
        let toolbarView = UIView()
        toolbarView.backgroundColor = .black.withAlphaComponent(0.7)
        view.addSubview(toolbarView)
        
        toolbarView.snp.makeConstraints { make in
            make.leading.trailing.bottom.equalToSuperview()
            make.height.equalTo(200.0)
        }
        
        // set the closure
        canvasView.getToolbarFrame = {
            return toolbarView.frame
        }
    }
    
}

Note that when we get the toolbar frame, we convert the rect to the coordinate space of the canvas view.

We want to do that because if the canvas view does not cover the entire view the coordinates will not be aligned.

Try this controller example to see -- it insets the canvas view so it's not "full screen" and it puts the toolbar view as a smaller rect not aligned with the full view:

class ViewController: UIViewController {
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.backgroundColor = .systemBlue
        
        let canvasView = CanvasView()
        canvasView.backgroundColor = .white
        view.addSubview(canvasView)
        
        canvasView.snp.makeConstraints { make in
            make.edges.equalToSuperview().inset(40)
        }
        
        let toolbarView = UIView()
        toolbarView.backgroundColor = .black.withAlphaComponent(0.7)
        view.addSubview(toolbarView)
        
        toolbarView.snp.makeConstraints { make in
            make.leading.trailing.bottom.equalToSuperview().inset(80)
            make.height.equalTo(200.0)
        }
        
        // set the closure
        canvasView.getToolbarFrame = {
            return toolbarView.frame
        }
    }
    
}

It will look like this:

enter image description here

If your beginning touch is on the white canvas view, the touchesMoved will begin tracking (printing the touch location). As you drag the touch around, if it enters the gray rect (the toolbar view) it will stop printing the coordinates... but will resume as you drag the touch back to the white area.