Search code examples
iosswifthittestuicontrol

Get tap event for UIButton in UIControl/rotatable view


Edited

See the comment section with Nathan for the latest project. There is only problem remaining: getting the right button.

Edited

I want to have a UIView that the user can rotate. That UIView should contain some UIButtons that can be clicked. I am having a hard time because I am using a UIControl subclass to make the rotating view and in that subclass I have to disable user interactions on the subviews in the UIControl (to make it spin) which may cause the UIButtons not be tappable. How can I make a UIView that the user can spin and contains clickable UIButtons? This is a link to my project which gives you a head start: it contains the UIButtons and a spinnable UIView. I can however not tap the UIButtons.

Old question with more details

I am using this pod: https://github.com/joshdhenry/SpinWheelControl and I want to react to a buttons click. I can add the button, however I can not receive tap events in the button. I am using hitTests but they never get executed. The user should spin the wheel and be able to click a button in one of the pie's.

Get the project here: https://github.com/Jasperav/SpinningWheelWithTappableButtons

See the code below what I added in the pod file:

I added this variable in SpinWheelWedge.swift:

let button = SpinWheelWedgeButton()

I added this class:

class SpinWheelWedgeButton: TornadoButton {
    public func configureWedgeButton(index: UInt, width: CGFloat, position: CGPoint, radiansPerWedge: Radians) {
        self.frame = CGRect(x: 0, y: 0, width: width, height: 30)
        self.layer.anchorPoint = CGPoint(x: 1.1, y: 0.5)
        self.layer.position = position
        self.transform = CGAffineTransform(rotationAngle: radiansPerWedge * CGFloat(index) + CGFloat.pi + (radiansPerWedge / 2))
        self.backgroundColor = .green
        self.addTarget(self, action: #selector(pressed(_:)), for: .touchUpInside)
    }
    @IBAction func pressed(_ sender: TornadoButton){
        print("hi")
    }
}

This is the class TornadoButton:

class TornadoButton: UIButton{
    override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {

        let pres = self.layer.presentation()!
        let suppt = self.convert(point, to: self.superview!)
        let prespt = self.superview!.layer.convert(suppt, to: pres)
        if (pres.hitTest(suppt)) != nil{
            return self
        }
        return super.hitTest(prespt, with: event)
    }

    override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
        let pres = self.layer.presentation()!
        let suppt = self.convert(point, to: self.superview!)
        return (pres.hitTest(suppt)) != nil
    }
}

I added this to SpinWheelControl.swift, in the loop "for wedgeNumber in"

wedge.button.configureWedgeButton(index: wedgeNumber, width: radius * 2, position: spinWheelCenter, radiansPerWedge: radiansPerWedge)
wedge.addSubview(wedge.button)

This is where I thought I could retrieve the button, in SpinWheelControl.swift:

override open func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
    let p = touch.location(in: touch.view)
    let v = touch.view?.hitTest(p, with: nil)
    print(v)
}

Only 'v' is always the spin wheel itself, never the button. I also do not see the buttons print, and the hittest is never executed. What is wrong with this code and why does the hitTest not executes? I rather have a normal UIBUtton, but I thought I needed hittests for this.


Solution

  • I was able to tinker around with the project and I think I have the solution to your problem.

    • In your SpinWheelControl class, you are setting the userInteractionEnabled property of the spinWheelViews to false. Note that this is not what you exactly want, because you are still interested in tapping the button which is inside the spinWheelView. However, if you don't turn off user interaction, the wheel won't turn because the child views mess up the touches!
    • To solve this problem, we can turn off the user interaction for the child views and manually trigger only the events that we are interested in - which is basically touchUpInside for the innermost button.
    • The easiest way to do that is in the endTracking method of the SpinWheelControl. When the endTracking method gets called, we loop through all the buttons manually and call endTracking for them as well.
    • Now the problem about which button was pressed remains, because we just sent endTracking to all of them. The solution to that is overriding the endTracking method of the buttons and trigger the .touchUpInside method manually only if the touch hitTest for that particular button was true.

    Code:

    TornadoButton Class: (the custom hitTest and pointInside are no longer needed since we are no longer interested in doing the usual hit testing; we just directly call endTracking)

    class TornadoButton: UIButton{
        override func endTracking(_ touch: UITouch?, with event: UIEvent?) {
            if let t = touch {
                if self.hitTest(t.location(in: self), with: event) != nil {
                    print("Tornado button with tag \(self.tag) ended tracking")
                    self.sendActions(for: [.touchUpInside])
                }
            }
        }
    }
    

    SpinWheelControl Class: endTracking method:

    override open func endTracking(_ touch: UITouch?, with event: UIEvent?) {
        for sv in self.spinWheelView.subviews {
            if let wedge = sv as? SpinWheelWedge {
                wedge.button.endTracking(touch, with: event)
            }
        }
        ...
    }
    

    Also, to test that the right button is being called, just set the tag of the button equal to the wedgeNumber when you are creating them. With this method, you will not need to use the custom offset like @nathan does, because the right button will respond to the endTracking and you can just get its tag by sender.tag.