Code Spelunking - DGRunkeeperSwitch
This post is the first of what I hope will be many that takes a piece of software that has been released publicly (usually on github) and looks at the code to learn more about it. I always learn a lot by reading other people’s code, and thought it would be fun to take various controls, apps, etc. and break them apart and point out what I find interesting about them.
The first control I’m choosing is the DGRunkeeperSwitch by Danil Gontovnik. I learned about it from a maniacdev email this morning, and I was immediately drawn in by the smooth animation and clean lines of the control. I use Runkeeper (less regularly than I used to due to the Apple Watch), but hadn’t noticed the control until I went looking for it. It is mainly used on screens other than the “Start” tab, which is where I spend most of my time. But, I don’t love the look of the standard segmented control and switch on iOS, so the more stylish look of this control is appealing.
First thing to consider when looking at this control is that it is written using Swift 2.0, so fire up the code in the latest XCode beta. It comes with a small sample app, which is a nice touch (and typical for most controls shared on github, I’ve found).
DGRunKeeperSwitch is a subclass of UIControl.
Swift notes
General coding notes
Side Trip
I was getting an unexpected error message when running the sample app, that was due to the UIViewControllerBasedStatusBarAppearance key being specified in the Info.plist file. This key had been set to NO to allow the view controller to set the color of the text in the status bar explicitly, using the call UIApplication.sharedApplication().statusBarStyle = .LightContent. Without the entry in the plist file, it doesn’t appear that this call actually changes the status bar color. The warning may just be an artifact of a beta release, but it bothered me. The way to fix this was to remove the key from the Info.plist file, and have the root view controller manage its own status bar style. However, the root view controller is actually a UINavigationController, and it does not, by default, defer to its top view controller for its status bar style, even though that would seem to be the obvious way to handle things. You can fix this in an extension to UINavigationController, which either overrides preferredStatusBarStyle() to return the custom status bar style, or in a more general way, override childViewControllerForStatusBarStyle() to return the topViewController, which then implements prefferedStatusBarStyle() itself. This seems like the more general option, and puts the responsibility to set the status bar color in the hands of the view controller that is presenting the rest of the UI, where presumably we are attempting to match an existing UI to the status bar color. One last Swift gush - it is so easy just to add a quick extension to a system class such as UINavigationController.
I didn’t think I would explore the world of status bar style overrides and system class extensions, but you never know what you’ll learn when you dive into a new piece of code!
Possibilities
It would be interesting to see this control support @IBDesignable/@IBInspectable so that it could be configured in IB. It also could be extended to support more than two choices, making it an alternative to UISegmentedControl in addition to UISwitch.
Conclusion
Overall, a lovely little control with clearly written code that takes advantage of some nice Swift language features to implement things in a clean, streamlined way.
The first control I’m choosing is the DGRunkeeperSwitch by Danil Gontovnik. I learned about it from a maniacdev email this morning, and I was immediately drawn in by the smooth animation and clean lines of the control. I use Runkeeper (less regularly than I used to due to the Apple Watch), but hadn’t noticed the control until I went looking for it. It is mainly used on screens other than the “Start” tab, which is where I spend most of my time. But, I don’t love the look of the standard segmented control and switch on iOS, so the more stylish look of this control is appealing.
First thing to consider when looking at this control is that it is written using Swift 2.0, so fire up the code in the latest XCode beta. It comes with a small sample app, which is a nice touch (and typical for most controls shared on github, I’ve found).
DGRunKeeperSwitch is a subclass of UIControl.
Swift notes
- Liberal use of tuples for assignment. Increases compactness of the code.
- Reasonable defaults for configurable aspects of the control, using defaults for variables on the control
- Uses the deinit method to remove itself as an observer. So nice to have a well-defined place to do this!
General coding notes
- Changes the underlying layer class of a few views in two different ways. The first, expected, way is by returning a custom class from the class-level layerClass() method. When your own class wants to change its layer, this works. If you want to change the layer class for a standard view, such as UIView, then you can use the Objective-C runtime method object_setClass to change the underlying class object. This new class object updates the cornerRadius of the view based on the height of the bounds. This is done in one list as a Swift didSet on the frame property of the CALayer. (I would almost count that as a clever Swift trick, since something like this is so much simpler to do in Swift than Objective-c). Also, can you really call it the Objective-C runtime when writing in Swift? I suppose just “runtime” is more appropriate now.
- The clever thing about this control is watching the color of the text labels change as you swipe the selected “bubble” from one item to the other. This is done by having two copies of each label, stacked on top of each other. They are in contrasting colors to their background. There is a background view between these two sets of labels, which contains the selection highlight. A mask is then applied to the view containing the “selected” labels (the top set of labels), such that the selected title views under the current selection background are visible, and the ones outside the bounds of the selection view are masked out.
- Both tap and pan gestures are supported. UIView dynamics are used for a nice springy animation when switching between choices. Snapping behavior kicks in if you pan only partway.
- layoutSubviews() handles positioning the selected rectangle above the selected item (essentially either on the right or left side of the control). If the labels are too long to fit in the specified size of the control, the label spills over its background, which doesn’t look great. Truncating the labels to fit would be better than letting them flow over the bounds of the control. This could be done by just pinning the size of each label to the maximum allowed size.
- The frame of the selection view and the frame of the mask view are tied together using KVO on the frame property of the selection background view. I’m not sure this is worth it, as you could easily set both frames at the same time, instead of decoupling this. Overall, it is less code to just assign the mask in the two places where it changes, although I can understand the desire to tie the two values together with KVO so that you don’t have to worry how many times the selection view is changed - it will always update the mask when its frame changes. Definitely a design choice, and one that I think could go either way.
Side Trip
I was getting an unexpected error message when running the sample app, that was due to the UIViewControllerBasedStatusBarAppearance key being specified in the Info.plist file. This key had been set to NO to allow the view controller to set the color of the text in the status bar explicitly, using the call UIApplication.sharedApplication().statusBarStyle = .LightContent. Without the entry in the plist file, it doesn’t appear that this call actually changes the status bar color. The warning may just be an artifact of a beta release, but it bothered me. The way to fix this was to remove the key from the Info.plist file, and have the root view controller manage its own status bar style. However, the root view controller is actually a UINavigationController, and it does not, by default, defer to its top view controller for its status bar style, even though that would seem to be the obvious way to handle things. You can fix this in an extension to UINavigationController, which either overrides preferredStatusBarStyle() to return the custom status bar style, or in a more general way, override childViewControllerForStatusBarStyle() to return the topViewController, which then implements prefferedStatusBarStyle() itself. This seems like the more general option, and puts the responsibility to set the status bar color in the hands of the view controller that is presenting the rest of the UI, where presumably we are attempting to match an existing UI to the status bar color. One last Swift gush - it is so easy just to add a quick extension to a system class such as UINavigationController.
I didn’t think I would explore the world of status bar style overrides and system class extensions, but you never know what you’ll learn when you dive into a new piece of code!
Possibilities
It would be interesting to see this control support @IBDesignable/@IBInspectable so that it could be configured in IB. It also could be extended to support more than two choices, making it an alternative to UISegmentedControl in addition to UISwitch.
Conclusion
Overall, a lovely little control with clearly written code that takes advantage of some nice Swift language features to implement things in a clean, streamlined way.