Customized UISlider with visual value tracking

5 Comments

Introduction

This is my first blog post on programming ever. So, please don’t be too picky;-) For a project i am currently working on i had to create a custom control which looks like a normal UISlider but provides a feedback to the user on which value he or she just set by moving it. Below you see how the final implementation is actually looking (Fig. 1). As a lazy programmer i first looked around and stumbled upon similarly looking ELCSlider described in the following blog-post. But i immediately had two complaints: it seemed to heavyweight and it was utilizing (or even abusing) the mighty UIPopoverViewController to create a small popup view showing the current slider value. So, i decided to go and implement the control myself with as less coding involved as possible.

Custom slider with popup showing current value

Fig. 1

Tracking touch events

To determine when the Value-Popup subview has to be shown (or dismissed) and when it has to be updated, i have overriden following UIControl methods in the UISlider subclass implementation:

#pragma mark - UIControl touch event tracking
 
- (BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event {
    // Fade in and update the popup view
    CGPoint touchPoint = [touch locationInView:self];
    // Check if the knob is touched. Only in this case show the popup-view
    if(CGRectContainsPoint(self.thumbRect, touchPoint)) {
        [self _positionAndUpdatePopupView];
        [self _fadePopupViewInAndOut:YES];
    }
    return [super beginTrackingWithTouch:touch withEvent:event];
}
 
- (BOOL)continueTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event {
    // Update the popup view as slider knob is being moved
    [self _positionAndUpdatePopupView];
    return [super continueTrackingWithTouch:touch withEvent:event];
}
 
- (void)cancelTrackingWithEvent:(UIEvent *)event {
    [super cancelTrackingWithEvent:event];
}
 
- (void)endTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event {
    // Fade out the popoup view
    [self _fadePopupViewInAndOut:NO];
    [super endTrackingWithTouch:touch withEvent:event];
}

InĀ  beginTrackingWithTouch: it is checked if the touch entered slider’s knob boundary. Only if it is the case the popup should be shown, since there is no endTrackingWithTouch: call following otherwise (e.g. if you touch the slider’s tracks).

Deriving a Popup-View position

The custom UISlider subclass includes a method which calculates a frame rectangle of the slider knob. This rectangle is then used as basis for calculating the popup-view frame by adding some offsets here and there:

- (CGRect)thumbRect {
    CGRect trackRect = [self trackRectForBounds:self.bounds];
    CGRect thumbR = [self thumbRectForBounds:self.bounds
                                         trackRect:trackRect
                                             value:self.value];
    return thumbR;
}

Implementing a Value-Popup UIView subclass

I would not go deep into the details of this particular subclass. You can download the corresponding Xcode project for further study. Let’s just have a quick look at the drawRect: method of this UIView subclass. It creates two paths using UIBezierPath (available starting with iOS 3.2+): rounded rectangle and arrow attached to it. Both paths are merged and filled with solid color. The float value representing slider knob position is assigned to a property and cached as a string used by drawRect. The popup-view is added as subview upon UISlider subclass initialization.

- (void)drawRect:(CGRect)rect {
 
    // Set the fill color
	[[UIColor colorWithWhite:0 alpha:0.8] setFill];
 
    // Create the path for the rounded rectanble
    CGRect roundedRect = CGRectMake(self.bounds.origin.x, self.bounds.origin.y, self.bounds.size.width, self.bounds.size.height * 0.8);
    UIBezierPath *roundedRectPath = [UIBezierPath bezierPathWithRoundedRect:roundedRect cornerRadius:6.0];
 
    // Create the arrow path
    UIBezierPath *arrowPath = [UIBezierPath bezierPath];
    CGFloat midX = CGRectGetMidX(self.bounds);
    CGPoint p0 = CGPointMake(midX, CGRectGetMaxY(self.bounds));
    [arrowPath moveToPoint:p0];
    [arrowPath addLineToPoint:CGPointMake((midX - 10.0), CGRectGetMaxY(roundedRect))];
    [arrowPath addLineToPoint:CGPointMake((midX + 10.0), CGRectGetMaxY(roundedRect))];
    [arrowPath closePath];
 
    // Attach the arrow path to the rounded rectangle
    [roundedRectPath appendPath:arrowPath];
 
    [roundedRectPath fill];
 
    // Draw the text
    if (self.text) {
        [[UIColor colorWithWhite:1 alpha:0.8] set];
        CGSize s = [_text sizeWithFont:self.font];
        CGFloat yOffset = (roundedRect.size.height - s.height) / 2;
        CGRect textRect = CGRectMake(roundedRect.origin.x, yOffset, roundedRect.size.width, s.height);
 
        [_text drawInRect:textRect
                 withFont:self.font
            lineBreakMode:UILineBreakModeWordWrap
                alignment:UITextAlignmentCenter];
    }
}

Conclusion

This post shows how you can create a UISlider which shows which value is set while you move the slider’s knob. It is a lightweight and can be used as drop-in replacement wherever you used UISlider view before. However there is a room for improvements and customizations. You can use the code in your own project and modify it as you like.

Check it out on GitHub or download Xcode project: ValueTrackingSlider.zip

{facebook-share} Bookmark and Share

5 Comments (+add yours?)

  1. Patrick Richards
    Apr 28, 2011 @ 13:05:22

    Great post. I downloaded the project and it works perfectly. I couldn’t help but make a few adjustments:

    1. In the _positionAndUpdatePopupView function, where the popupRect is calculated it would often come up with a non-’floored’ value for the height, which causes drawing problems. That is, if the height was (say) 18.5 instead of 18.0, it results in the arrow looking blurry, as in your screenshot. I changed the line to:

    CGRect popupRect = CGRectOffset(_thumbRect, 0, floorf(-(_thumbRect.size.height * 1.5)));

    A screenshot of before and after makes it more obvious: http://i.imgur.com/T7lv7.png

    I also made the same changes in the code used to calculate the height of the roundedRect in drawRect. There is a WWDC 2009 video that explains what is going on here but I can’t remember which one.

    2. I added in some code to take into account when the popup will move outside of it’s view and get cutoff. In the _positionAndUpdatePopupView function I just calculate when the rect will be outside of it’s bounds, and adjust it accordingly. I also save the offset in a new property, and use that to offset the arrow in the drawRect: function so it still stays in line with the sider thumb. (i.e., the popover box stays within the frame, but the arrow continues to move)

    3. In the – (BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event function where the touch is detected, I added a fudge factor using CGRectInset as I noticed it was possible to grab the thumb from slightly outside the edge and move it and the popup wouldn’t appear.

    I only meant to fix the first issue but then couldn’t help myself! Here is the updated project:

    http://domesticcat.com.au/code/ValueTrackingSlider-Modified.zip

    I hope it’s useful to you. By the way, you should put your code up on GitHub, others would find it useful.

  2. admin
    Apr 28, 2011 @ 13:29:43

    Hi Patrick,

    thanks a lot for sharing your changes. Will have a look and eventually update the project. Putting it on GitHub, i guess, is also a good idea.

    Michael

  3. Paul
    Apr 29, 2011 @ 09:29:24

    Thanks to both of you. Really good and useful stuff that can be extrapolated on especially for those of us still finding our way. Kudos!

  4. sirdan
    Jan 12, 2012 @ 04:20:54

    Nice. This works well. Thanks.

  5. Nikolaj Schumacher
    Jul 05, 2012 @ 22:37:19

    I suggest calling super *before* updating the popup position. Otherwise the popup is always one frame behind.

Leave a Reply

*