UISlider and UIScrollView
Asked Answered
H

7

17

I have a UISlider as part of a view that is loaded into a UIScrollView with paging enabled. I've noticed an unexpected behavior. If the user tries to use the slider quickly (i.e. press and move) it "activates" the scroll view, causing the page to switch. However, if your press and hold for a second the slider "activates" and you can then adjust the slider value. This behavior is undesirable.

What is the best way to make the UISlider responsive when loaded into a UIScrollView? I've thought about adding a "blocker" view that just eats up touch events that is placed under the slider, but not sure if this is the best way to go about it.

Hoof answered 5/1, 2011 at 2:21 Comment(0)
B
3

Have your scroll view detect touches over the region occupied by your slider (override hitTest:withEvent:, you may need to subclass UIScrollView). If a touch over said region is detected, tell your scroll view to immediately pass the touch to the slider.

Bluebird answered 5/1, 2011 at 2:24 Comment(2)
Thanks, ended up going with that. However, another oddity... this behavior only seemed to manifest on the simulator. Once I got it on the device it worked fine without the fix.Hoof
FWIW, I'm seeing exactly the same behavior (as described in the original question) on both simulator (iOS 6) and device (iOS 5.1). I have to tap and hold a moment before dragging the slider, or else I get scroll view drag instead.Assets
C
46

there's no need for a hit test on the UIScrollView side. since the delay is set by the UIScrollView itself. subclassing and implementing a custom hitTest:withEvent: won't help since it's still triggered with delay.

i searched hours for an elegant solution to this, since i wanted to simulate apple's own volumeslider in the ios application switcher.

the trick:

yourScrollView.delaysContentTouches = NO;

unfortunately this disables events along the UISliders track, so for this part your UIScrollView won't trigger any touchevents because they are caught by the slider first.

to pass touchevents other than those which are in the UISliders thumb rect you have to subclass UISlider and add the following:

// get the location of the thumb
- (CGRect)thumbRect 
{
   CGRect trackRect = [self trackRectForBounds:self.bounds];
   CGRect thumbRect = [self thumbRectForBounds:self.bounds
                                  trackRect:trackRect
                                      value:self.value];
   return thumbRect;
}

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{   
   CGRect thumbFrame = [self thumbRect];

      // check if the point is within the thumb
   if (CGRectContainsPoint(thumbFrame, point))
   {
      // if so trigger the method of the super class
      NSLog(@"inside thumb");
      return [super hitTest:point withEvent:event];
   }
   else
   {
      // if not just pass the event on to your superview
      NSLog(@"outside thumb");
      return [[self superview] hitTest:point withEvent:event];
   }
}
Clamor answered 22/6, 2011 at 20:3 Comment(1)
Thanks Sebastian, that's exactly what I was looking for!Kowtow
B
3

Have your scroll view detect touches over the region occupied by your slider (override hitTest:withEvent:, you may need to subclass UIScrollView). If a touch over said region is detected, tell your scroll view to immediately pass the touch to the slider.

Bluebird answered 5/1, 2011 at 2:24 Comment(2)
Thanks, ended up going with that. However, another oddity... this behavior only seemed to manifest on the simulator. Once I got it on the device it worked fine without the fix.Hoof
FWIW, I'm seeing exactly the same behavior (as described in the original question) on both simulator (iOS 6) and device (iOS 5.1). I have to tap and hold a moment before dragging the slider, or else I get scroll view drag instead.Assets
E
2

While creating an audio player I had the exact problem with a sliderView and it is added to a scrollView , and whenever I touched the sliderView , the scrollView used to get the touch and instead of the sliderView , the scrollView moved . To avoid this , I disabled the srcolling of the scrollView when the user touched the sliderView and otherwise the scrolling is enabled .

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {

    UIView *result = [super hitTest:point withEvent:event] ;
    if (result == sliderView)
    {
        scrollView.scrollEnabled = NO ;
    }
    else
    {
        scrollView.scrollEnabled = YES ;
    }

    return result ;

}
Electrocorticogram answered 19/11, 2013 at 6:52 Comment(1)
where you added this method ?Determinative
H
2

My problem was a superset of this issue - I've got UISliders inside UITableViewCells and the whole UITableView is a page inside a UIScrollView. The sliders were wreaking havoc on the interactions with the other two and the subclassing solutions did not work. Here's what I came up with that's working great: send notifications when the sliders move and have the UITableView and UIScrollView disableScrolling on during this time. Note in the picture below: my sliders are horizontal, my tableview is vertical, and my UIScrollView has horizontal pages.

UISlider in a UITableView in a UIScrollView

The UITableViewCell picks up events for the programmatically-created slider:

self.numberSlider = [[UISlider alloc] init];
[self.numberSlider addTarget:self action:@selector(sliderValueChanged:) forControlEvents:UIControlEventValueChanged];
[self.numberSlider addTarget:self action:@selector(sliderTouchDown:) forControlEvents:UIControlEventTouchDown];
[self.numberSlider addTarget:self action:@selector(sliderTouchUp:) forControlEvents:UIControlEventTouchUpInside];
[self.numberSlider addTarget:self action:@selector(sliderTouchUp:) forControlEvents:UIControlEventTouchUpOutside];

For the purposes of this tutorial, we only care about touchDown and Up:

- (void)sliderTouchDown:(UISlider *)sender
{
    [[NSNotificationCenter defaultCenter] postNotificationName:NOTIFY_SLIDER_TOUCH_BEGAN object:nil];
}

- (void)sliderTouchUp:(UISlider *)sender
{
    [[NSNotificationCenter defaultCenter] postNotificationName:NOTIFY_SLIDER_TOUCH_ENDED object:nil];
}

Now, we catch in these notifications in both the UITableView (note that the tableview is in a VC but I'm sure this would work if you subclassed):

- (void)viewDidLoad
{
    // other stuff
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(sliderTouchDown:) name:NOTIFY_SLIDER_TOUCH_BEGAN object:nil];
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(sliderTouchUp:) name:NOTIFY_SLIDER_TOUCH_ENDED object:nil];
}

- (void)sliderTouchDown:(NSNotification *)notify
{
    self.treatmentTableView.scrollEnabled = NO;
}

- (void)sliderTouchUp:(NSNotification *)notify
{
    self.treatmentTableView.scrollEnabled = YES;
}

and the UIScrollView (same as above, enclosed in a VC):

- (void)viewDidLoad
{
    // other stuff

    // Register for slider notifications
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(disableScrolling:) name:NOTIFY_SLIDER_TOUCH_BEGAN object:nil];
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(enableScrolling:) name:NOTIFY_SLIDER_TOUCH_ENDED object:nil];
}

- (void)disableScrolling:(NSNotification *)notify
{
    self.scrollView.scrollEnabled = NO;
}

- (void)enableScrolling:(NSNotification *)notify
{
    self.scrollView.scrollEnabled = YES;
}

I'd love to hear a more elegant solution, but this one definitely gets the job done. When you use a slider, the table and scrollviews hold still and when you click outside the sliders, the tableview and scrollviews move as expected. Also - notice that I could use non-subclassed instances of all 3 components in this solution. Hope this helps someone!

Hilliary answered 22/1, 2014 at 0:43 Comment(0)
N
2

I wanted to post this as a comment, but my account has no rep so I had to post a whole answer. user762391's answer including the hitTest override works perfectly. I just want to add that instead of overriding hitTest, you could instead override pointInside:withEvent: like this (keeping the thumbRect method):

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent * _Nullable)event {
    return CGRectContainsPoint([self thumbRect], point)
}

or in Swift:

var thumbRect: CGRect {
    return thumbRectForBounds(bounds, trackRect: trackRectForBounds(bounds), value: value)
}

override func pointInside(point: CGPoint, withEvent event: UIEvent?) -> Bool {
    return thumbRect.contains(point)
}
Neill answered 7/10, 2015 at 17:1 Comment(1)
Thank You so much, Jeff. I was trying to solve this issue for the last 2 hours. Your solution worked for me. I just have to update it for Swift 5. var thumbRect: CGRect { return thumbRect(forBounds: bounds, trackRect: trackRect(forBounds: bounds), value: value) } override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { return thumbRect.contains(point) }Declarative
R
2

most upvoted comment code in Swift 3 :)

import Foundation

class UISliderForScrollView:UISlider {
    var thumbRect:CGRect {
        let trackRect = self.trackRect(forBounds: bounds)
        return thumbRect(forBounds: bounds, trackRect: trackRect, value: value)
    }

    override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
        if thumbRect.contains(point) {
            return super.hitTest(point, with: event)
        } else {
            return superview?.hitTest(point, with: event)
        }
    }
}
Reduplicative answered 18/1, 2017 at 10:53 Comment(1)
Thanks It actually saved me for converting the objective - c code in swiftCrucifix
C
0

You can subclass UIScrollView and override the method - (BOOL)touchesShouldBegin:(NSSet *)touches withEvent:(UIEvent *)event inContentView:(UIView *)view as follows:

- (BOOL)touchesShouldBegin:(NSSet *)touches withEvent:(UIEvent *)event inContentView:(UIView *)view {
    if ([view isKindOfClass:[UISlider class]]) {
        UITouch *touch = [[event allTouches] anyObject];
        CGPoint location = [touch locationInView:view];
        CGRect thumbRect;
        UISlider *mySlide = (UISlider*) view;
        CGRect trackRect = [mySlide trackRectForBounds:mySlide.bounds];
        thumbRect = [mySlide thumbRectForBounds:mySlide.bounds trackRect:trackRect value:mySlide.value];
        if (CGRectContainsPoint(thumbRect, location))
            return YES;
    }
    return NO;
}

Also Set yourScrollView.delaysContentTouches = NO; property for the scrollview.

Cliffcliffes answered 22/5, 2013 at 12:26 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.