Hello, and thanks for stopping by my blog! This marks my first post as part of iDevBlogADay. If you’ve never heard of it, you should go and check it out. It’s basically a group of indie iOS developers who are committed to blogging on a regular basis about all the experiences of being an indie developer – everything from marketing tips, to code examples, to graphic design, to just about anything else you can think of. If you’ve found my blog through iDevBlogADay, then I just want to say a special thanks for stopping by to see one of the “new guys.” Hopefully you’ll find something here that’s helpful or encouraging, no matter what stage you’re in as an indie developer. Just a quick intro to me: I’m a relative newcomer to the developer scene and have been doing iOS development for just over two full years, with no other substantial programming experience prior to that. Currently I’ve got one app for sale on the app store (plus the Lite version) called theDrumDictionary. If you’re interested in that or more info about me and my background, you can find it on the previous posts in this blog. As of right now, this app and basically all my current ideas for future apps are focused on the area of tools for musicians. So if you have a particular interest in music or music apps, be sure to stop back by – or subscribe to the feed – and I’ll try to keep the blog posts coming with everything from specific music and audio programming things I’ve learned, to more widely applicable topics like interface design and code.
For this post, I’d like to share a bit about the major interface control I’m working on for my upcoming metronome app. The basic idea is a rotary control that allows users to select between different values or options by scrolling around in a circle rather than simply scrolling up and down on a UITableView or UIPickerView.
The basic idea is inspired by the beautiful Convertbot by the Tapbots crew. The circular scroller idea itself goes back even further to the many generations of click wheel iPods prior to the rise of the iPhone and multitouch.
It’s a fairly complicated mechanism, and there’s a lot that I could share about the implementation and why I chose this format, but I think I’ll save that for later posts. For now, how about a quick intro to the world of UIViews, CALayers and transforms that form the basis of my version of this control.
Circles are Freakin’ Hard!
If you’ve done any work on interface design on iOS then you’ve surely made use of the UIKit framework. For basic layout and presentation of views, UIView and its various iterations – UIImageView, UIScrollView, etc. – are pretty simple and straightforward. There’s a whole slew of properties you can access to set sizes and positions of views, as well as background color and alpha, and to top it all off, many properties are animatable. With very little code, you can get some pretty complicated looks and have all kinds of things flying in and out, zipping around, fading in and out, growing and shrinking, whatever you want to do. Combine this with the concept of adding views as subviews of others, and you can do all sorts of interesting layouts and animation with minimal effort. The first sort of problem we run into though is the fact that everything, and I mean everything, in the UIKit system is based around rectangles. CGRects to be exact. Every view has a square frame, is positioned within its superview in relation to the superview’s square shape, is defined in terms of rectangular sizes, etc. If you want to deal with circles – or any shape other than a rectangle for that matter – and handle touches, layout, etc. you’re going to have to take on some extra burden to get things looking and acting the way you want. Core Graphics, of course, offers methods for getting circles drawn onto the screen, or you can use pre made images with transparency, but either way, we’re still talking about a circle sitting inside a square UIView.
The other major problem you’ll notice is that in all of the available properties for UIView, there is no “rotation.” Well, that’s a problem because this whole concept is based on nothing BUT rotation, and laying out views at various angles around a circle. For rotation, we have to turn to the UIView’s “transform” property. This property is of the type CGAffineTransform and allows us to take the underlying view hierarchy and model and transform it in various ways so that when it comes time to draw to the screen, the view has been altered. Views can be translated, scaled, and rotated. Even if you don’t understand the math behind matrix transforms – and I certainly don’t – you can still make good use of the CGAffineTransform using some Apple-provided functions. For rotation there’s CGAffineTransformRotate, which takes an existing transform as it’s first parameter and then rotates it by the angle (in radians, not degrees) given as the second parameter, adding to whatever translation, scale or rotation was already a part of the transform. Or, there’s CGAffineTransformMakeRotation, which will return a new transformation with only rotation at the given angle. Now that we’ve found the functions we need, for this post, we’ll just focus on getting a rudimentary number dial running. It’s not gonna be pretty, but it’ll get the job done. First thing to do is decide how many numbers we want to show and create the views to display them. I’m just going to use UILabels. So here’s what it looks like after the first step of just adding the main circle image to the view and adding 9 number labels as subviews of the circle in the center of the image. I gave the labels a background color to better see what going on, and an alpha of .2 so you can see that right now, it’s 9 labels all right on top of each other in the center. Now, to get them arranged around the circle, we’ll need to apply the rotation transform. Since we’ve got 9 labels, each label will be rotated 2π/9 radians more than the previous one to fill out the entire circle evenly. UIViews rotate around their centers, so in order to arrange the labels around the edge, we can make sure each label is the same width as the circle, then apply the rotation to get this:
Now, it looks OK, but you can easily tell that it’s not ideal. (Unless of course you’re making a kaleidoscope app!) All of these multiple overlapping views mean extra work for the rendering system, not to mention all the just plain waste of having each view extend all the way across the circle.
Cleaning It Up
How can we clean this up? The answer is to dive just a little bit deeper and access the “layer” property of the labels. Every UIView is backed by a CALayer that’s responsible for the actual drawing. UIView adds support for touch handling and presents a different set of API’s to interact with the drawing and animations, but behind every UIView, by default, is a CALayer. If you’ve got a developer login and can access the 2011 WWDC videos, I highly recommend the one called “Understanding UIKit Rendering.” It has a great chart that details some of the shared properties between UIView and CALayer and how they are accessed in each, as well as some of the differences. One property on CALayer that cannot be accessed from UIView is the anchorPoint. The anchorPoint is a CGPoint that defines the exact spot within the view’s own bounds that acts as its anchor to its superview. Confusing right? Maybe this will help: think of the layer as a sheet of paper that you’re pinning to a bulletin board. The anchorPoint is the spot on the piece of paper where the pin will be pushed through. The position property of CALayer is the spot where the pin will be stuck on the bulletin board.
By default the anchorPoint is set to the center: (0.5, 0.5) – 50% of the way across in both the x and y directions. The exact relationship between UIView’s frame, bounds, center and CALayer’s bounds, position, and anchorPoint can be difficult to understand at first, but the most important thing for us right now is to know that setting a CALayer’s anchorPoint means any rotation will be applied around that point, again, just like a piece of paper stuck to a cork board will rotate around the pin. So, first, let’s cut the width of our labels in half and position them at the left edge. Then, we’ll set the anchorPoint to (1.0, 0.5) – the far right side and centered vertically. Now, when we apply the rotation transformation, the labels line up the exact same way as before, but without all the unnecessary size and extra overlapping.
Take out the red color, and we’ve got a decent start to a number dial. If we want to rotate the whole dial, apply a rotation transform on the whole circle image. Since the labels are subviews of the circle, they will rotate right along with it and also maintain their relative positions because of their own rotations. Aren’t transforms fun!!
Two things to note: 1) CALayer also has a transform property, but it’s separate and of a completely different type than the UIView’s transform property. BUT, since the UIView’s drawing is done in its CALayer, we can modify the layer’s anchorPoint property, and still see the results we’re looking for when we rotate the UIView’s transform. It’s all a bit confusing, but very powerful. 2) If you’re not already doing it, in order to access the CALayer API’s you’ll need to link against the QuartzCore framework and import that header in your file. Alright, I think that’s enough for now. Here’s the code on Pastebin for the final version if you’d like to check it out. Add it to viewDidLoad or similar, and don’t forget to clean up the memory (I was lazy for the example)! Happy transforming!
Head to Part 2, for adding rotation based on touch input.
Here’s a look at this concept “in action” as a tempo selector in my app.
The design is far from finished, but you can at least get an idea of what the control looks like. As this is my first iDevBlogADay attempt, I’d love to hear your feedback about the post: too long, too boring, too simple, did I get something wrong? Let me know what you’d like to read about! Thanks!