The treat app makes use of a wheel based selection menu. This is used for selecting occasions, photos and moods. I have tried to explain the reasoning behind the implementation of this, as shown below.
If you want a sample implementation of this project its here: https://github.com/kmdarshan/wheel
Below are some of the reasons, we had to rewrite the wheel:
a. The number of wheels which could be instantiated was fixed. Only two wheels could be instantiated.
b. Memory consumption was huge, which resulted in other complications ( not able to launch camera ).
c. Filter values was fixed. e.g. If we set the filter values as Dad, Mom, Son and released it to the app store, these values could not be changed.If these had to be changed, we needed to do this on the front end and release it again, which means we lose another week.Keeping in mind with the dynamic configuration of categories, this was a major change needed to be done.
d. Filter wheels did not do a complete 360 degree rotation.
Approaches considered when implementing the wheel:
1. Bezier Curve – Using a bezier curve to draw the entire words as a single string.
Advantage: By using this approach, we just needed to calculate the four points, to draw the bezier curve.
The bezier curve in turn will draw the curved text for us automatically.
Disadvantage: Although this sounds like a good idea, the entire view is redrawn every time the wheel is rotated.
Also the redrawing wasn’t exactly giving us the feel of a smooth rotating wheel.
2. UIView – Drawing the entire string in a single view using CoreText.
Advantage: Single view will be faster to render and use. No hassles of maintaining multiple views and detecting multiple touches.
Disadvantage: Since the words are dynamic, there can be many words. Every time a view is visible, we need to draw it, since the entire view is a single string,
we need to manipulate this string. Essentially we are redrawing the entire view. This takes a lot of memory space.
3. Multiple views to draw each word.
Advantage: We went this approach where, each word is drawn in a single view. This will help us remove view individually when they are not visible and add views when they become visible.
Disadvantage: Since each word will have its own view, managing them becomes a pain point. We need to implement caching for multiple views since just creating more and more views will bring us to square one (memory issues).
Implementation details from the ground up :
a. Getting the curve for each word –
Look at the image carefully. We are making use of coretext to rotate the string. Once the string is rotated, we place it on a view.
Once this is done, we clip the view based on the given dimensions. The implementation is in TPCurvedTextView.h/.m
b. Rotation –
https://redflowerinc.com/wp-content/uploads/2015/02/s3.png?w=255
Once this is done for all the words, we take each word and set the background as clear color. This will give us a perfect curved word. Next we rotate this view based on the position its in the bigger circle. Something similar to the image shown above. The rotation can be achieved using CGAffineTransformMakeRotation;
c. Gestures or Touches:
This was a tough decision whether to go with Gestures or touches (UIControl). For those not familiar with gestures and touches here is a quick recap.
iOS apps recognize combinations of touches and respond to them in ways that are intuitive to users, such as zooming in on content in response to a pinching gesture and scrolling through content in response to a flicking gesture. When you configure these controls, they send an action message to a target object when that touch occurs. You can also employ the target-action mechanism on views by using gesture recognizers. When you attach a gesture recognizer to a view, the entire view acts like a control—responding to whatever gesture you specify.
Coming to touches, you can think of this as a level below gestures. You need to override these methods manually for it to work, whereas if you use gestures all this is handled by the OS itself:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event;
We decided to go with touches since its fits our purpose. When a user swipes his finger diagonally towards the bottom, we should be moving the clockwise, in the same way, moving it anti clockwise, when the finger is moved in the opposite direction. If we had used just gestures, it would have become a bit complex to detect such user movements. Hence we went with just the touches where we detect movement to be either anti clockwise or clockwise, which aligns with the directions a wheel can rotate.
d. Displaying the wheels {}
To give the illusion that a view is in the form of a circle, we use the corner radius property of the wheel (read it here:http://developer.apple.com/library/ios/#DOCUMENTATION/GraphicsImaging/Reference/CALayer_class/Introduction/Introduction.html#//apple_ref/occ/cl/CALayer ).
This works fine if we just want to display the wheels and have limited interaction.
Lets consider the below figure:
The green circle is the wheel after applying the corner radius. The green circle is the only one which is visible to the user. But any user touches within the circle is detected. This should be fine, if we just have one circle.
Consider the figure below, if we have more than one circle as shown below. In this figure, circle one is above circle two here.
Due to how touches work, if the user touches at the corners of the circle one, thinking its circle two, we will be moving the wrong wheel, which is circle one. This would be a bad user experience. At this juncture, drawRect and bezier curve comes to our rescue. At first, we draw the bezier path circle, for the three wheels. The code is as shown below :
aPath = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 0, radius*2, radius*2) cornerRadius:radius]
// Set the render colors.
[[UIColor whiteColor] setStroke];
[wheelBackgroundColor setFill];
[aPath addClip];
The bezier curve basically forms the foundation of the three wheels. Next we detect the if the user touch is within the bezier circle and not the entire view. This will help us eliminate the above scenario. The code to that would be:
– (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
return [aPath containsPoint:point];
}
On top of the bezier path, we display the individual views, which would display the filter values.
Leave a Reply