A Circular Slider

Following up on our last post another kind of slider came to mind. This one is a staple of the macOS world as well and for macOS also directly available in Xcode’s storyboard editor. And again – the equivalent for iOS is sadly missing in action. We are talking about the circular slider of course. It seems a bit non-obvious, but this control is indeed a descendant of the slider family. The macOS NSSlider has a simple toggle to switch a regular strip slider into this round control which has the not only benefit of being very compact, but due to its square dimensions makes it ideal for layout orientation switches between landscape and portrait. We already have most of the basic building blocks needed and again will derive this custom control from UIControl.

The basic logic for a circular slider – for those wondering how the “round” movement is implemented – is to treat all positive x and y movement as increments and negative x or y movement as decrement. That works reasonably well and is intuitively understood by users. On macOS round sliders typically only implement the y-axis, but there the user can drag out of the window if there is insufficient space above the control. Not an option on iOS which is why we implement the x-axis as well.

For the implementation we are going to continue our journey with Xcode’s IBDesignable and IBInspectable feature. Let it be said that the designable feature seems to be based on an older code base and therefore extremely finicky. It can be outright frustrating to work with and is very hard to debug if errors occur. So let’s address one of the most common errors right up-front. It typically presents in pairs:

Failed to render and update auto layout status for ViewController (BYZ-38-t0r): The agent crashed

Main.storyboard: error: IB Designables: Failed to update auto layout status: The agent crashed

These errors are atypical in that a project will still compile and run. They just indicate that the storyboard editor cannot display a view as intended. The most common reason for this error is a problem in the code base for the rendering view. Fixing it will not remove the errors right away even after cleaning a project and/or removing all files in the Derived Data directory. It seems that a helper app Xcode is spawning in a background thread is only called every couple of minutes or so and it is solely responsible for removing the errors. So upon encountering these bad boys first check the draw method in the rendering control and then give the storyboard editor a chance to refresh itself. And as always when working with IBDesignable make sure you have “Automatically Refresh Views” in the Editor menu checked.

In last month’s post on vertical sliders we drew the entire thing in code. That’s fine, but of course  not always ideal. Graphics are often imported as .png files and would it not be nice if we could have the storyboard editor allow us to pick images for background and thumb from those already included in the project? Well, we can, but there is a little trick to it. An inspectable CGFloat or color is often declared with a default value like so:

Knowing that we might be tempted to do something similar for an UIImage:

This will compile without any issues and render everything as intended, but we will not see any interface in the storyboard inspector to let us pick images. In fact there will be no entry for baseImg at all. This is unexpected and one might at first assume that images are not supported as designables. But then we know that the regular UISlider allows for image customization of the thumb part So what gives? Well, the trick is to declare the image as optional. This may be an undocumented feature and eventually stop working. But we are still OK with Xcode 8 and 9. So this is how we can “trick” Xcode into allowing image selections in the storyboard:

 

The Code

There is not a lot new here as compared to our vertical slider control. As discussed we have a block of inspectables at the very top of our class:

This is then followed by 5 variables:

These are fairly self-explanatory. Suffice to say that we have a new sensitivity setting that comes in handy and then there is the didSet section on the value parameter which is near-identical to that of our vertical slider.

Things get a bit more interesting in the draw call of our control. Again this time we are using images and don’t draw anything ourselves. The 2 images that we are using are 70×70 pixels in dimension and while there is nothing special about the background image the way the thumb is implemented is somewhat note-worthy. It resembles an apostrophe in shape and is located at the 12 o’clock position of the circular background. This image is also rendered with a transparent background.

In order to have the thumb follow the outlines of the circle we will use radians which are easier to visualize than degrees in code: CGFloat.pi / 180. 0 in our system is 12 o’clock and corresponds to 0 radians and every hour tick on the clock adds another 30 radians going clockwise. Our control will track from 8 o’clock to 4 o’clock so we set a track length value of 240 and a track start value also of 240. To make things easy to change if needed we put these values in two let variables at the top of the class. There is not much sense in making these inspectable as they would not yield any visual feedback in the storyboard editor.

Drawing the background for our control is very straightforward and can be done with one line of code:

Next is the thumb image with rotation. We are rendering in a CGContext which we can rotate. It doesn’t support setting a custom pivot point though and will rotate around its zero point in the top left. So we have to translate the context before rotating it and offset the rectangle we are drawing into as well. Before doing all that we push another instance of the draw context with saveGState and when done restore the context:

 

Final Thoughts

Phew – that was a lot of detail for barely 120 lines of code. Inspectables are a great Xcode feature and really help out during interface design. Let’s hop that Apple will continue to support it and maybe even get to a point where they add the much requested capability of having inspectable font settings. Something that is sorely missed.

The full final project is available on GitHub. Happy coding everyone :]