iosobjective-cuislider

UISlider thumb to move without touching the thumb


I have a custom UISlider inside UICollectionViewCell. I would like to be able to move the slider thumb by touching anywhere on the cell (on the purple bit). For instance, if I was to put my finger on the top right corner of the cell and slide down, I would like the thumb to slide downwards from its current position (-19.0 in this case), instead of jumping up first.

enter image description here

Here is my code:

@implementation CDCFaderSlider

- (id)initWithFrame:(CGRect)frame {
    self = [super initWithFrame: frame];
    
    if (self) {
        [self setOpaque: true];
        [self setTransform: CGAffineTransformMakeRotation((CGFloat)(M_PI * -0.5f))];
        [self constructSlider];
    }
    return self;
}

- (id)initWithCoder:(NSCoder *)aDecoder {
    self = [super initWithCoder: aDecoder];
    
    if (self) {
        [self setTransform: CGAffineTransformMakeRotation((CGFloat)(M_PI * -0.5f))];
        [self constructSlider];
    }
    return self;
}

#pragma mark - UIControl touch event tracking

- (BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event {
    CGPoint touchPoint = [touch locationInView: self];
    
    if (CGRectContainsPoint(CGRectInset(self.thumbRect, -12.0, -12.0), touchPoint)) {
        [self positionAndUpdateValueView];
        [self fadeValueViewInAndOut: true];
    }
    return [super beginTrackingWithTouch: touch withEvent: event];
}

- (BOOL)continueTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event {
    [self positionAndUpdateValueView];
    return [super continueTrackingWithTouch: touch withEvent: event];
}

- (void)cancelTrackingWithEvent:(UIEvent *)event {
    [self fadeValueViewInAndOut: false];
    [super cancelTrackingWithEvent: event];
}

- (void)endTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event {
    [self fadeValueViewInAndOut: false];
    [super endTrackingWithTouch: touch withEvent: event];
}

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
    [self fadeValueViewInAndOut: false];
    [super touchesEnded: touches withEvent: event];
}

- (BOOL) pointInside:(CGPoint)point withEvent:(UIEvent*)event {
    CGRect bounds = self.bounds;
    bounds = CGRectInset(bounds, 0, -70);
    return CGRectContainsPoint(bounds, point);
}

#pragma mark - Helper methods

- (void)constructSlider {
    self.valueView = [[CDCFaderValueView alloc] initWithFrame: CGRectZero];
    self.valueView.backgroundColor = UIColor.clearColor;
    self.valueView.alpha = 0.0;
    self.valueView.transform = CGAffineTransformMakeRotation((CGFloat)(M_PI * 0.5));
    
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        [self addSubview: self.valueView];
    });
}

- (void)fadeValueViewInAndOut:(BOOL)aFadeIn {
    [UIView animateWithDuration: 0.5 animations:^{
        if (aFadeIn) {
            self.valueView.alpha = 0.8;
        } else {
            self.valueView.alpha = 0.0;
        }
    } completion:^(BOOL finished){
    }];
}

- (void)positionAndUpdateValueView {
    CGRect ThumbRect = self.thumbRect;
    CGRect popupRect = CGRectOffset(ThumbRect, (CGFloat)floor(ThumbRect.size.width * 0.2), (CGFloat) - floor(ThumbRect.size.height * 0.5));
    self.valueView.frame = CGRectInset(popupRect, -30, -10);
    self.valueView.value = faderDBValues[(NSInteger)self.value];
}

#pragma mark - Property

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

- (void)sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event {
    if (self.count > 4) {
        [super sendAction: action to: target forEvent: event];
        self.count = 0;
    } else {
        self.count++;
    }
    
    if (self.tracking == 0) {
        [super sendAction: action to: target forEvent: event];
    }
}

@end

Solution

  • I would highly recommend writing a custom UIControl subclass, but, if you want to stick with the transformed UISlider ...

    1 - we can't limit begin tracking to only a touch inside the thumb

    2 - we need to track the start touch point and update the slide value based on the y-coordinate "delta" from start touch to continued touch

    Here's your class, with a few changes:

    CDCFaderSlider.h

    #import <UIKit/UIKit.h>
    
    NS_ASSUME_NONNULL_BEGIN
    
    @interface CDCFaderSlider : UISlider
    
    @end
    
    NS_ASSUME_NONNULL_END
    

    CDCFaderSlider.m - note: you didn't provide your CDCFaderValueView class, so I just implemented a simple one at the top...

    #import "CDCFaderSlider.h"
    
    @interface CDCFaderValueView : UIView
    @property (assign, readwrite) float value;
    @end
    @implementation CDCFaderValueView
    @end
    
    @interface CDCFaderSlider ()
    {
        double curVal;
        CGPoint startPT;
    }
    @property (strong, nonatomic) CDCFaderValueView *valueView;
    @property (assign, readwrite) NSInteger count;
    
    @end
    
    @implementation CDCFaderSlider
    
    - (id)initWithFrame:(CGRect)frame {
        self = [super initWithFrame: frame];
        
        if (self) {
            [self setOpaque: true];
            [self setTransform: CGAffineTransformMakeRotation((CGFloat)(M_PI * -0.5f))];
            [self constructSlider];
        }
        return self;
    }
    
    - (id)initWithCoder:(NSCoder *)aDecoder {
        self = [super initWithCoder: aDecoder];
        
        if (self) {
            [self setTransform: CGAffineTransformMakeRotation((CGFloat)(M_PI * -0.5f))];
            [self constructSlider];
        }
        return self;
    }
    
    #pragma mark - UIControl touch event tracking
    
    - (BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event {
        // get touch point in superview
        CGPoint touchPoint = [touch locationInView: self.superview];
    
        // is the touch inside my frame?
        if (CGRectContainsPoint(self.frame, touchPoint)) {
            [self positionAndUpdateValueView];
            [self fadeValueViewInAndOut: true];
            // save start point
            startPT = touchPoint;
            // save current value
            curVal = self.value;
            return YES;
        }
        
        return [super beginTrackingWithTouch: touch withEvent: event];
    }
    
    - (BOOL)continueTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event {
        // get touch point in superview
        CGPoint touchPoint = [touch locationInView: self.superview];
        
        // get the y-coordinate movement
        double yDelta = startPT.y - touchPoint.y;
        // update value to the saved value plus
        //  the yDelta as a percentage of frame height
        self.value = curVal + (yDelta / self.frame.size.height);
        
        [self positionAndUpdateValueView];
        return YES;
    }
    
    - (void)cancelTrackingWithEvent:(UIEvent *)event {
        [self fadeValueViewInAndOut: false];
        [super cancelTrackingWithEvent: event];
    }
    
    - (void)endTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event {
        [self fadeValueViewInAndOut: false];
        [super endTrackingWithTouch: touch withEvent: event];
    }
    
    - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
        [self fadeValueViewInAndOut: false];
        [super touchesEnded: touches withEvent: event];
    }
    
    - (BOOL) pointInside:(CGPoint)point withEvent:(UIEvent*)event {
        CGRect bounds = self.bounds;
        bounds = CGRectInset(bounds, 0, -70);
        return CGRectContainsPoint(bounds, point);
    }
    
    #pragma mark - Helper methods
    
    - (void)constructSlider {
        self.valueView = [[CDCFaderValueView alloc] initWithFrame: CGRectZero];
        self.valueView.backgroundColor = UIColor.cyanColor;
        self.valueView.alpha = 0.0;
        self.valueView.transform = CGAffineTransformMakeRotation((CGFloat)(M_PI * 0.5));
        
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            [self addSubview: self.valueView];
        });
    }
    
    - (void)fadeValueViewInAndOut:(BOOL)aFadeIn {
        [UIView animateWithDuration: 0.5 animations:^{
            if (aFadeIn) {
                self.valueView.alpha = 0.8;
            } else {
                self.valueView.alpha = 0.0;
            }
        } completion:^(BOOL finished){
        }];
    }
    
    - (void)positionAndUpdateValueView {
        CGRect ThumbRect = self.thumbRect;
        CGRect popupRect = CGRectOffset(ThumbRect, (CGFloat)floor(ThumbRect.size.width * 0.2), (CGFloat) - floor(ThumbRect.size.height * 0.5));
        self.valueView.frame = CGRectInset(popupRect, -30, -10);
        //self.valueView.value = faderDBValues[(NSInteger)self.value];
    }
    
    #pragma mark - Property
    
    - (CGRect)thumbRect {
        CGRect trackRect = [self trackRectForBounds: self.bounds];
        CGRect thumbR = [self thumbRectForBounds: self.bounds trackRect: trackRect value: self.value];
        
        return thumbR;
    }
    
    - (void)sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event {
        if (self.count > 4) {
            [super sendAction: action to: target forEvent: event];
            self.count = 0;
        } else {
            self.count++;
        }
        
        if (self.tracking == 0) {
            [super sendAction: action to: target forEvent: event];
        }
    }
    
    @end
    

    Example controller:

    SliderTestViewController.h

    #import <UIKit/UIKit.h>
    
    NS_ASSUME_NONNULL_BEGIN
    
    @interface SliderTestViewController : UIViewController
    
    @end
    
    NS_ASSUME_NONNULL_END
    

    SliderTestViewController.m

    #import "SliderTestViewController.h"
    #import "CDCFaderSlider.h"
    
    @interface SliderTestViewController ()
    @property (strong, nonatomic) CDCFaderSlider *slider;
    @property (strong, nonatomic) UIView *outlineView;
    @end
    
    @implementation SliderTestViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        
        self.view.backgroundColor = UIColor.systemYellowColor;
        
        self.slider = [CDCFaderSlider new];
        self.slider.translatesAutoresizingMaskIntoConstraints = NO;
        [self.view addSubview:self.slider];
        
        UILayoutGuide *g = self.view.safeAreaLayoutGuide;
        
        [NSLayoutConstraint activateConstraints:@[
            [self.slider.widthAnchor constraintEqualToConstant:500.0],
            [self.slider.heightAnchor constraintEqualToConstant:150.0],
            [self.slider.centerXAnchor constraintEqualToAnchor:g.centerXAnchor],
            [self.slider.centerYAnchor constraintEqualToAnchor:g.centerYAnchor],
        ]];
    
        // because the slider get's transformed, let's add an "outline view"
        //  to show the actual frame of the slider
        self.outlineView = [UIView new];
        self.outlineView.userInteractionEnabled = NO;
        self.outlineView.layer.borderColor = UIColor.redColor.CGColor;
        self.outlineView.layer.borderWidth = 1.0;
    }
    
    - (void)viewDidLayoutSubviews {
        [super viewDidLayoutSubviews];
        
        // only execute if outlineView has not already been added
        if (!self.outlineView.superview) {
            [self.view addSubview:self.outlineView];
            self.outlineView.frame = self.slider.frame;
        }
    }
    
    @end
    

    Looks like this when running - the red outline shows the frame of the transformed slider:

    running

    You can touch-drag anywhere in the red rectangle to move the thumb.