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.
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
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:
You can touch-drag anywhere in the red rectangle to move the thumb.