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.
The general solution would be to use a
UIView
and place all yourUIButton
s where they should be, and use aUIPanGestureRecognizer
to rotate your view, calculate speed and direction vector and rotate your view. For rotating your view I suggest usingtransform
because it's animatable and also your subviews will be also rotated. (extra: If you want to set direction of yourUIButtons
always downward, just rotate them in reverse, it will cause them to always look downward)Hack
Some people also use
UIScrollView
instead ofUIPanGestureRecognizer
. Place described View inside theUIScrollView
and useUIScrollView
's delegate methods to calculate speed and direction then apply those values to yourUIView
as described. The reason for this hack is becauseUIScrollView
decelerates speed automatically and provides better experience. (Using this technique you should setcontentSize
to something very big and relocatecontentOffset
ofUIScrollView
to.zero
periodically.But I highly suggest the first approach.
Here is a solution for your specific project:
Step 1
In the
drawWheel
function inSpinWheelControl.swift
, enable user interaction on thespinWheelView
. To do this, remove the following line:Step 2
Again in the
drawWheel
function, make the button a subview of thespinWheelView
, not thewedge
. Add the button as a subview after the wedge, so it will appear on top of the wedge shape layer.Old:
New:
Step 3
Create a new
UIView
subclass that passes touches through to its subviews.Step 4
At the very beginning of the
drawWheel
function, declare thespinWheelView
to be of typePassThroughView
. This will allow the buttons to receive touch events.With those few changes, you should get the following behavior:
(The message is printed to the console when any button is pressed.)
Limitations
This solution allows the user to spin the wheel as usual, as well as tap any of the buttons. However, this might not be the perfect solution for your needs, as there are some limitations:
Depending on your needs, you might consider building your own spinner instead of relying on a third-party pod. The difficulty with this pod is that it is using the
beginTracking(_ touch: UITouch, with event: UIEvent?)
and related functions instead of gesture recognizers. If you used gesture recognizers, it would be easier to make use of all theUIButton
functionality.Alternatively, if you just wanted to recognize a touch down event within the bounds of a wedge, you could pursue your
hitTest
idea further.Edit: Determining which button was pressed.
If we know the
selectedIndex
of the wheel and the startingselectedIndex
, we can calculate which button was pressed.Currently, the starting
selectedIndex
is 0, and the button tags increase going clockwise. Tapping the selected button (tag = 0), prints 7, which means that the buttons are "rotated" 7 positions in their starting state. If the wheel started in a different position, this value would differ.Here is a quick function to determine the tag of the button that was tapped using two pieces of information: the wheel's
selectedIndex
and thesubview.tag
from the currentpoint(inside point: CGPoint, with event: UIEvent?)
implementation of thePassThroughView
.Again, this is definitely a hack, but it works. If you are planning to continue to add functionality to this spinner control, I would highly recommend creating your own control instead so you can design it from the beginning to fit your needs.
I was able to tinker around with the project and I think I have the solution to your problem.
SpinWheelControl
class, you are setting theuserInteractionEnabled
property of thespinWheelView
s tofalse
. Note that this is not what you exactly want, because you are still interested in tapping the button which is inside thespinWheelView
. However, if you don't turn off user interaction, the wheel won't turn because the child views mess up the touches!touchUpInside
for the innermost button.endTracking
method of theSpinWheelControl
. When theendTracking
method gets called, we loop through all the buttons manually and callendTracking
for them as well.endTracking
to all of them. The solution to that is overriding theendTracking
method of the buttons and trigger the.touchUpInside
method manually only if the touchhitTest
for that particular button was true.Code:
TornadoButton Class: (the custom
hitTest
andpointInside
are no longer needed since we are no longer interested in doing the usual hit testing; we just directly callendTracking
)SpinWheelControl Class:
endTracking
method: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 theendTracking
and you can just get its tag bysender.tag
.As for my opinion, you can use your own view with few sublayers and all other stuff you need. In this case u will get full flexibility but you also should write a little bit more code.
If you like this option u can get something like on gif below (you can customize it as u wish - add text, images, animations etc):
Here I show you 2 continuous pan and one tap on purple section - when tap is detected6 bg color changed to green
To detect tap I used
touchesBegan
as shown below.To play with code for this you can copy-paste code below in to playground and modify as per your needs