Update: Super glad to have this control accepted by CocoaControls! Thanks guys! http://www.cocoacontrols.com/controls/roulettewheelcollectionview
This would probably be the last post I’ll write with reference to the Yahoo TimeTraveler app, and I want to make it interesting by showing how the implementation of a custom UIControl evolved throughout time.
The control we were trying to build was a roulette wheel of city cards. I guess the idea came from spinning a globe and deciding on your next travel destination based on where your finger landed. Simple enough, but when it came to building the damn thing it was anything but.
A video of the control in action, from release v2.0.2
The app started life as a hybrid app with most of its UI done with HTML. When the page began to load, an initialization method looped through an array of elements, calculated each element’s rotation angle (n/360), and applied it to the element’s style attribute.
The next step was to create a swipe gesture recognizer on the main div element, and add logic to animate the cards on both sides in and out of the viewport when a swipe occurs.
As seen in the clip below, this approach seemed to work pretty well. But there were obvious issues like jerkiness in the scrolling, missed strokes, and absence of inertia. It had a long way to go to reach the smoothness and speed of custom controls like iTunes’ coverflow.
Second Try – Going Native
There was one big caveat. In the original design, the cards on the right had to be always stacked on the top. I tried to do so in scrollViewDidScroll:, using the UIImageView’s frame origin to work out which cards were in the viewport. However, I didn’t realize that the frame size of a UIView would be distorted after a transformation, so determining whether a transformed cell lied within a certain boundary was super difficult.
The idea of using UIView animation and GestureRecognizers also backfired. It was near impossible to cancel on-going animations and start new ones based on user input. Suppose a user does a quick flick, then taps on a card. He would expect the wheel to stop spinning and the card slowly lock to the upright position when the touch was released. Firing off UIView animations wasn’t the way to do it. On the other hand, not using UIView animations meant there was no easy way to add inertia to the scroll.
Third Try – Using a UIScrollView
With little point in pursuing that design further, I turned to my then-manager for advice. “It doesn’t necessarily have to be a wheel, it just have to look like one”. He said.
That got me thinking in a different direction.
He suggested looking into implementations of coverflow, which can be interpreted to having very similar requirements as we did, with the major difference being coverflow making use of 3D perspective transforms and us 2D rotations. There is also the added advantage of having silk smooth animations (with inertia, can’t stress that enough) backed by the almighty UIScrollview.
Restrained from coding right away, I prototyped on paper and in Keynote (unorthodox, but super effective) — laying out the cards in a horizontal table (slide 1), applying varying degrees of rotation to each (slide 2), and figuring out what levels of translations were needed to make the cards appear like a wheel (slide 3). I then needed to find the offsets so I can deduce where the cards were by covering the design with a 1:1 replica of the iPhone chrome (slide 4).
After getting comfortable with the constraints time was spent building a prototype. Unfortunately the code can’t be shown here but they were the usual UITableViewCell and UITableView subclasses. To prevent cards from “popping in” when views were queued and de-queued, the viewport had to be extended a bit, 50px on each side to be exact.
Here’s the version we shipped with. We must have gone through 5-6 builds until we all felt pleased with it.
Fourth Try – UICollectionView
The seed for re-implementing the roulette wheel control in UICollectionView had been planted in my head since I saw the demo in WWDC. When I actually got round to doing it, it was rather straightforward thanks to tutorials from Ray Wenderlich and various github projects. Subclassing UICollectionViewFlowLayout and overriding layoutAttributesForElementsInRect:rect was all I had to do.
The layout subclass where 99% of the logic resided:
Using UICollectionView was awesome. Not only was the animation super smooth (helped by the god-send shouldInvalidateLayoutForBoundsChange:), and you get features like center snapping for free. It drastically reduced the amount of custom code I had to write, and offered far greater performance with FPS going through the roof. UICollectionView was definitely the greatest developer feature of iOS6.
A little bit of LOC comparison:
UIView – ? (code was thrown away)
UIScrollView – 945 lines
UICollectionFlowLayout – 213 lines
To close off, here’s what I learned from the year long experience of implementing this custom iOS control with different approaches.
- Work from native UIKit elements where possible.
- Cheat! Yes, really. There’s a difference between cutting corners and doing something clever.
Remember, it doesn’t necessarily have to be a wheel, it just need to look like one.
A demo app of the UICollectionView version of the wheel can be found at https://github.com/kenshin03/RouletteWheelCollectionViewDemo.
- Reinventing the wheel on iPhone – http://mobilefriendly.wordpress.com/2012/03/06/reinventing-the-wheel-on-iphone/.
- Session 205 – Introducing Collection Views
- Session 219 – Advanced Collection Views and building custom layouts