Search code examples
iosswiftuigesturerecognizerxcode11bezier

Tap issue with curve views in iOS


I have a requirement to create a view that has a top and bottom view merged where the top view needs to have a curve at the bottom and both the top and bottom view performs different actions on tap. I was able to create this view as below

enter image description here

This is the screenshot of my storyboard constraints

enter image description here

This is my curve view class

class CurveView: UIView {

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }

    override func layoutSubviews() {
        super.layoutSubviews()
        self.createBottomCurve()
    }

    private func createBottomCurve() {
        let offset: CGFloat = self.frame.width / 1.2
        let bounds: CGRect = self.bounds
        let rectBounds: CGRect = CGRect(x: bounds.origin.x, y: bounds.origin.y, width: bounds.size.width, height: bounds.size.height / 2)
        let rectPath: UIBezierPath = UIBezierPath(rect: rectBounds)
        let ovalBounds: CGRect = CGRect(x: bounds.origin.x - offset / 2, y: bounds.origin.y, width: bounds.size.width + offset, height: bounds.size.height)
        let ovalPath: UIBezierPath = UIBezierPath(ovalIn: ovalBounds)
        rectPath.append(ovalPath)
        let maskLayer: CAShapeLayer = CAShapeLayer()
        maskLayer.frame = bounds
        maskLayer.path = rectPath.cgPath
        self.layer.mask = maskLayer
    }
}

And this is the implementation in viewcontroller class

class ViewController: UIViewController, UIGestureRecognizerDelegate {

    @IBOutlet weak var greenCurveView: CurveView!
    @IBOutlet weak var yellowView: UIView!

    override func viewDidLoad() {
        super.viewDidLoad()
        let greenViewTap = UITapGestureRecognizer(target: self, action: #selector(handleGreenViewTap(_:)))
        greenViewTap.delegate = self
        greenCurveView.addGestureRecognizer(greenViewTap)
        let yellowViewTap = UITapGestureRecognizer(target: self, action: #selector(handleYellowTap(_:)))
        yellowViewTap.delegate = self
        yellowView.addGestureRecognizer(yellowViewTap)
    }

    @objc func handleGreenViewTap(_ pan: UITapGestureRecognizer ){
        print("Green View is tapped")
    }

    @objc func handleYellowTap(_ pan: UITapGestureRecognizer ){
        print("Yellow View is tapped")
    }

}

This works fine except in the scenario where we tap yellow view in the highlighted rectangle area in red in the below image. In the below image, if I click on the yellow area in rectangle it takes the tap of green view which makes sense since the yellow view is behind green view. So, how do I determine the yellow area tap in this region? Any help is appreciated

enter image description here


Solution

  • What you need is to instruct CurveView where it can be tapped, in this case within its own path:

    override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
           return rectPath.contains(point)
    }
    

    So overall your CurveView could look like this:

    class CurveView: UIView {
    
        var rectPath = UIBezierPath()
    
        required init?(coder aDecoder: NSCoder) {
            super.init(coder: aDecoder)
        }
    
        override func layoutSubviews() {
            super.layoutSubviews()
            self.createBottomCurve()
        }
    
        private func createBottomCurve() {
            let offset: CGFloat = self.frame.width / 1.2
            let bounds: CGRect = self.bounds
            let rectBounds: CGRect = CGRect(x: bounds.origin.x, y: bounds.origin.y, width: bounds.size.width, height: bounds.size.height / 2)
            rectPath = UIBezierPath(rect: rectBounds)
            let ovalBounds: CGRect = CGRect(x: bounds.origin.x - offset / 2, y: bounds.origin.y, width: bounds.size.width + offset, height: bounds.size.height)
            let ovalPath: UIBezierPath = UIBezierPath(ovalIn: ovalBounds)
            rectPath.append(ovalPath)
            let maskLayer: CAShapeLayer = CAShapeLayer()
            maskLayer.frame = bounds
            maskLayer.path = rectPath.cgPath
            self.layer.mask = maskLayer
        }
    
        override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
               return rectPath.contains(point)
        }
    }
    

    Output: