iosuiviewuibutton

UIButton that allows clicks to pass through to button below


enter image description here

I create this image for illustration of my issue.

I have a button that on touch hold changes background to pink. Then a view in-between the two buttons to display a view. Then another button on top to add an inner shadow.

I cannot get both targets to work at the same time.

Is this even achievable?

I tried using HitTest without success, as it pass the click to the button below but then then first button loses its target.

I am considering maybe using UIViews to achieve the same effect as I am using Layers inside the button's target to achieve the effect that I am after.

I just need two views on top of each other that can receive touches simultaneously. I would prefer a UIButton solution as the hold touch is almost instantaneous whilst a LongPress gesture takes some time to take effect.

I have been on this problem for quite a while now so I decided to ask some help.


Solution

  • You can (probably) accomplish this easier by using a UIView and handling touches rather than using a UIButton ...

    First, let's create a basic "change background on touch" view:

    class HighlightView: UIView {
        
        // very light gray
        var normalColor: UIColor = UIColor(red: 0.95, green: 0.95, blue: 0.95, alpha: 1.0)
        
        // light pink
        var hightlightColor: UIColor = UIColor(red: 1.0, green: 0.9, blue: 1.0, alpha: 1.0)
        
        override init(frame: CGRect) {
            super.init(frame: frame)
            commonInit()
        }
        required init?(coder: NSCoder) {
            super.init(coder: coder)
            commonInit()
        }
        private func commonInit() {
            backgroundColor = normalColor
        }
        func doHighlight(_ b: Bool) {
            backgroundColor = b ? hightlightColor : normalColor
        }
        override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
            backgroundColor = hightlightColor
        }
        override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
            guard let t = touches.first else { return }
            let pt = t.location(in: self)
    
            // if touch is inside my bounds
            //  use highlight color
            // else, touch moved outside so
            //  use normal color
            
            if bounds.contains(pt) {
                backgroundColor = hightlightColor
            } else {
                backgroundColor = normalColor
            }
        }
        override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
            backgroundColor = normalColor
        }
        override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
            backgroundColor = normalColor
        }
        
    }
    

    with an example view controller:

    class TouchHoldVC: UIViewController {
        
        let holdView = HighlightView()
        
        override func viewDidLoad() {
            super.viewDidLoad()
            
            view.backgroundColor = .systemBackground
            
            holdView.translatesAutoresizingMaskIntoConstraints = false
            view.addSubview(holdView)
            
            let g = view.safeAreaLayoutGuide
            NSLayoutConstraint.activate([
                holdView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
                holdView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
                holdView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
                holdView.heightAnchor.constraint(equalToConstant: 200.0),
            ])
            
        }
        
    }
    

    On init we set the background color to "normal" ...

    looks like this:

    enter image description here


    Next we can create an "inner shadow view" class:

    class InnerShadowView: UIView {
        
        // "inner" shadow
        private let innerShadowLayer = CAShapeLayer()
        private let innerShadowMaskLayer = CAShapeLayer()
        
        override init(frame: CGRect) {
            super.init(frame: frame)
            commonInit()
        }
        required init?(coder: NSCoder) {
            super.init(coder: coder)
            commonInit()
        }
        
        private func commonInit() -> Void {
            
            // add sublayer
            self.layer.addSublayer(innerShadowLayer)
    
            // fillColor doesn't matter - just needs to be opaque
            innerShadowLayer.fillColor = UIColor.white.cgColor
            innerShadowLayer.fillRule = .evenOdd
    
        }
        
        override func layoutSubviews() {
            
            super.layoutSubviews()
            
            // for the "inner" shadow,
            // rectangle path needs to be larger than
            //  bounds + shadow offset + shadow raidus
            // so the shadow doesn't "bleed" from all sides
            let path = UIBezierPath(rect: bounds.insetBy(dx: -40, dy: -40))
            
            // create a path for the "hole" in the layer
            let holePath = UIBezierPath(rect: bounds)
            
            // this "cuts a hole" in the path
            path.append(holePath)
            path.usesEvenOddFillRule = true
            
            innerShadowLayer.path = path.cgPath
            
            // mask the layer, so we only "see through the hole"
            innerShadowMaskLayer.path = holePath.cgPath
            innerShadowLayer.mask = innerShadowMaskLayer
            
            // adjust properties as desired
            innerShadowLayer.shadowOffset = .zero
            innerShadowLayer.shadowColor = UIColor.black.cgColor
            innerShadowLayer.shadowRadius = 5
            
            // setting .shadowOpacity to a very small value (such as 0.025)
            //  results in very light shadow
            // set .shadowOpacity to 1.0 to clearly see
            //  what the shadow is doing
            innerShadowLayer.shadowOpacity = 0.75
            
        }
        
    }
    

    Modify HighlightView by adding InnerShadowView as a subview:

    class HighlightView: UIView {
        
        var normalColor: UIColor = UIColor(red: 0.95, green: 0.95, blue: 0.95, alpha: 1.0)
        var hightlightColor: UIColor = UIColor(red: 1.0, green: 0.9, blue: 1.0, alpha: 1.0)
        
        let shadView = InnerShadowView()
        
        override init(frame: CGRect) {
            super.init(frame: frame)
            commonInit()
        }
        required init?(coder: NSCoder) {
            super.init(coder: coder)
            commonInit()
        }
        private func commonInit() {
            
            shadView.translatesAutoresizingMaskIntoConstraints = false
            addSubview(shadView)
            
            let g = self
            NSLayoutConstraint.activate([
                shadView.topAnchor.constraint(equalTo: g.topAnchor, constant: 0.0),
                shadView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
                shadView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
                shadView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: 0.0),
            ])
            
            backgroundColor = normalColor
            shadView.isHidden = true
        }
        
        func doHighlight(_ b: Bool) {
            backgroundColor = b ? hightlightColor : normalColor
            shadView.isHidden = !b
            
        }
        override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
            doHighlight(true)
        }
        override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
            guard let t = touches.first else { return }
            let pt = t.location(in: self)
            if bounds.contains(pt) {
                doHighlight(true)
            } else {
                doHighlight(false)
            }
        }
        override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
            doHighlight(false)
        }
        override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
            doHighlight(false)
        }
            
    }
    

    using that same example controller, now:

    Looks like this:

    enter image description here


    Next, a simple "content view" with an image and a label:

    class SomeContentView: UIView {
    
        let label = UILabel()
        let imgView = UIImageView()
        
        override init(frame: CGRect) {
            super.init(frame: frame)
            commonInit()
        }
        required init?(coder: NSCoder) {
            super.init(coder: coder)
            commonInit()
        }
        private func commonInit() {
            label.translatesAutoresizingMaskIntoConstraints = false
            addSubview(label)
            
            imgView.translatesAutoresizingMaskIntoConstraints = false
            addSubview(imgView)
            
            let pad: CGFloat = 8.0
            let g = self
            NSLayoutConstraint.activate([
                label.topAnchor.constraint(equalTo: g.topAnchor, constant: pad),
                label.leadingAnchor.constraint(equalTo: imgView.trailingAnchor, constant: 12.0),
                label.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -16.0),
                label.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -pad),
                
                imgView.topAnchor.constraint(equalTo: g.topAnchor, constant: pad),
                imgView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: pad),
                //imgView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
                imgView.widthAnchor.constraint(equalToConstant: 80.0),
                imgView.heightAnchor.constraint(equalTo: imgView.widthAnchor, multiplier: 1.0),
                imgView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -pad),
            ])
            
            label.numberOfLines = 0
            label.textAlignment = .center
            label.text = "This an image view and a label, in a view we will call \"SomeContentView\""
            
            if let img = UIImage(named: "test300x300") {
                imgView.image = img
            }
        }
    
    }
    

    by itself, it looks like this:

    enter image description here

    we'll add a function to our HighlightView -- func addContent(_ cView: UIView) -- so we can add that as a subview:

    class HighlightView: UIView {
        
        var normalColor: UIColor = UIColor(red: 0.95, green: 0.95, blue: 0.95, alpha: 1.0)
        var hightlightColor: UIColor = UIColor(red: 1.0, green: 0.9, blue: 1.0, alpha: 1.0)
        
        let shadView = InnerShadowView()
        
        override init(frame: CGRect) {
            super.init(frame: frame)
            commonInit()
        }
        required init?(coder: NSCoder) {
            super.init(coder: coder)
            commonInit()
        }
        private func commonInit() {
            
            shadView.translatesAutoresizingMaskIntoConstraints = false
            addSubview(shadView)
            
            let g = self
            NSLayoutConstraint.activate([
                shadView.topAnchor.constraint(equalTo: g.topAnchor, constant: 0.0),
                shadView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
                shadView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
                shadView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: 0.0),
            ])
            
            backgroundColor = normalColor
            shadView.isHidden = true
        }
        
        func doHighlight(_ b: Bool) {
            backgroundColor = b ? hightlightColor : normalColor
            shadView.isHidden = !b
            
        }
        override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
            doHighlight(true)
        }
        override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
            guard let t = touches.first else { return }
            let pt = t.location(in: self)
            if bounds.contains(pt) {
                doHighlight(true)
            } else {
                doHighlight(false)
            }
        }
        override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
            doHighlight(false)
        }
        override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
            doHighlight(false)
        }
        
        func addContent(_ cView: UIView) {
            
            cView.translatesAutoresizingMaskIntoConstraints = false
            insertSubview(cView, at: 0)
            
            let g = self
            NSLayoutConstraint.activate([
                cView.topAnchor.constraint(equalTo: g.topAnchor, constant: 0.0),
                cView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
                cView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
                cView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: 0.0),
            ])
            
        }
        
    }
    

    and a couple changes to our sample controller -- we'll add an instance of SomeContentView and let its constraints set the height:

    class TouchHoldVC: UIViewController {
        
        let holdView = HighlightView()
        
        override func viewDidLoad() {
            super.viewDidLoad()
            
            view.backgroundColor = .systemBackground
            
            holdView.translatesAutoresizingMaskIntoConstraints = false
            view.addSubview(holdView)
            
            let g = view.safeAreaLayoutGuide
            NSLayoutConstraint.activate([
                holdView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
                holdView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
                holdView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
                // the content view will set the height of the "touch hold" Highlight view
                //holdView.heightAnchor.constraint(equalToConstant: 200.0),
            ])
            
            holdView.addContent(SomeContentView())
    
        }
        
    }
    

    and we get this:

    enter image description here