Search code examples
swiftlayeruibezierpathcashapelayer

make only points within UIBezierPath button selectable


My swift code features a custom shape button made from a UIBezierPath. The code should only call the func press if it the user toches the red part if the user touches the green part the func press should not be called. Right now if you touch the green part the func is still called.

enter image description here

import UIKit

class ViewController: UIViewController {
    let customButton = UIButton(frame: CGRect(x: 100.0, y: 100.0, width: 200.0, height: 140.0))
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        
        customButton.backgroundColor = UIColor.green
        
        let aPath = UIBezierPath()
        
        aPath.move(to: CGPoint(x: 50, y: 0))
        
        aPath.addLine(to: CGPoint(x: 200.0, y: 40.0))
        aPath.addLine(to: CGPoint(x: 160, y: 140))
        aPath.addLine(to: CGPoint(x: 40.0, y: 140))
        aPath.addLine(to: CGPoint(x: 0.0, y: 40.0))
        
        aPath.close()
        
        let layer = CAShapeLayer()
        layer.fillColor = UIColor.red.cgColor
        layer.strokeColor = UIColor.red.cgColor
        layer.path = aPath.cgPath
        
        customButton.layer.addSublayer(layer)
        
        self.view.addSubview(customButton)
        customButton.addTarget(self, action: #selector(press), for: .touchDown)
        
    }
    
    @objc func press(){
        print("hit")
    }
    
    
}

Solution

  • My solution to the problem is to subclass UIButton into a FunkyButton. That button contains the path information. FunkyButton overrides the hitTest(_:event:) method, and checks if the point is contained in the path.
    Give this a whirl:

    class ViewController: UIViewController {
        let customButton = FunkyButton(frame: CGRect(x: 100.0, y: 100.0, width: 200.0, height: 140.0))
        
        override func viewDidLoad() {
            super.viewDidLoad()
            // Do any additional setup after loading the view.
            
            customButton.backgroundColor = UIColor.green
    
            self.view.addSubview(customButton)
            customButton.addTarget(self, action: #selector(press), for: .touchDown)        
        }
        
        @objc func press(){
            print("hit")
        }
    
    }
    
    class FunkyButton: UIButton {
        let aPath = UIBezierPath()
    
        override init (frame : CGRect) {
            super.init(frame : frame)
            aPath.move(to: CGPoint(x: 50, y: 0))
    
            aPath.addLine(to: CGPoint(x: 200.0, y: 40.0))
            aPath.addLine(to: CGPoint(x: 160, y: 140))
            aPath.addLine(to: CGPoint(x: 40.0, y: 140))
            aPath.addLine(to: CGPoint(x: 0.0, y: 40.0))
    
            aPath.close()
            let shapedLayer = CAShapeLayer()
            shapedLayer.fillColor = UIColor.red.cgColor
            shapedLayer.strokeColor = UIColor.red.cgColor
            shapedLayer.path = aPath.cgPath
    
            layer.addSublayer(shapedLayer)
        }
        
        required init?(coder aDecoder: NSCoder) {
            super.init(coder: aDecoder)
        }
        
        override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
            if self.isHidden == true || self.alpha < 0.1 || self.isUserInteractionEnabled == false {
                return nil
            }
            if aPath.contains(point) {
                return self
            }
            return nil
        }
    }