Henry F
Henry F

Reputation: 4980

drawRect is not drawing over a UI element

TL/DR version: I'm trying to draw a line OVER the UISlider, not under it.

I'm overriding the UISlider's drawRect method in order to draw lines upon it. For some reason, the sliders progress bar overlaps the line I draw, and it appears like so:

enter image description here

You can BARELY see the marks, hence my need to draw differently. This is the code I wrote to draw said lines:

- (void)drawRect:(CGRect)rect {

    CGContextRef context = UIGraphicsGetCurrentContext();

    CGContextSetStrokeColorWithColor(context, [UIColor whiteColor].CGColor);
    CGContextSetLineWidth(context, 20);

    for (NSNumber *x in self.lines) {
        CGContextMoveToPoint(context, x.floatValue + 175, 45);
        CGContextAddLineToPoint(context, x.floatValue + 175, rect.size.height - 50);
        CGContextStrokePath(context);
    }
}

I figured that would work just fine, but nope. So, I did some research on this website, and the conclusion that I can to is like I need to override layoutSubViews.

Perfect, so I added [super layoutSubviews]; right in the beginning on my method; Like so:

- (void)drawRect:(CGRect)rect {
    [super layoutSubviews];
    ... // all other code
}

No help. As you can tell from the code, I'm setting the line color as [UIColor whiteColor], yet it appears so hard to see, even when I override layoutSubViews. I would like it to look like this instead:

enter image description here

NOTE- For anyone trying to do this or similar, as rmaddy stated below:

"Overriding the drawRect: method of a control provided by Apple is a really bad idea. It's fragile and likely to break in some iOS update."

I'm doing this as a quick patch, and it's NOT best practice to do this if you don't need to.

Upvotes: 1

Views: 866

Answers (2)

Sandeep
Sandeep

Reputation: 21134

It just happens so that the drawing you do are behind the minimum and maximum track image view inside UISlider. You can do it so that you create a separate view, say suppose LineView, and then add it to your UISlider, bring this subView to the top most zIndex and do line drawing to this view. See the implementation I have done to get more idea about how you can achieve.

@interface LineView: UIView

@property (nonatomic, strong) NSArray *lines;

@end

@implementation LineView

- (void)setLines:(NSArray *)lines
 {
    _lines = lines;
    [self setNeedsDisplay];
 }

- (instancetype)initWithFrame:(CGRect)frame
{
    if (self = [super initWithFrame:frame]) {
        self.opaque = YES;
        self.backgroundColor = [UIColor clearColor];
    }
    return self;
}

// disable the touch as UISlider has thumb view as a separate view which actually needs to handle touch events
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
    return NO;
}

- (void)drawRect:(CGRect)rect
{
    [super drawRect:rect];
    const CGFloat lineWidth = 5;
    CGContextRef context = UIGraphicsGetCurrentContext();

    CGContextSetStrokeColorWithColor(context, [UIColor redColor].CGColor);
    CGContextSetLineWidth(context, lineWidth);

    for (NSNumber *x in self.lines) {
        CGContextMoveToPoint(context, x.floatValue , self.bounds.size.height);
        CGContextAddLineToPoint(context, x.floatValue + 20, self.bounds.size.height);

        CGContextStrokePath(context);
    }
}


@end


@interface MySlider : UISlider

  @property (nonatomic, strong) UIView *lineView;
  @property (nonatomic, strong) NSArray *lines;

@end

@implementation MySlider

- (void)setLines:(NSArray *)lines
{
  self.lineView.lines = lines;
}

- (NSArray *)lines
{
  return self.lineView.lines;
}

- (instancetype)initWithFrame:(CGRect)frame
{
    if (self = [super initWithFrame:frame]) {
        self.lineView = [[LineView alloc] initWithFrame:self.bounds];
        [self addSubview:self.lineView];
    }
    return self;
}

- (void)layoutSubviews
{
    [super layoutSubviews];
    [self bringSubviewToFront:self.lineView];
}

@end

And set it from external interface.

mySlider.lines = @[@0, @50, @100, @160, @180, @250];

And here is the result,

enter image description here

Upvotes: 1

Aaron Golden
Aaron Golden

Reputation: 7102

The UI elements are subviews of your view. drawRect fills in the contents of the view itself, but its subviews draw on top of the view. If you want to draw over the view and its subviews, you'll need to add your own subviews to the slider, or embed the whole slider in a separate parent view, that has its own subviews on top of the slider.

You can see the view hierarchy of the slider in the debugger, using the recursiveDescription method:

(lldb) po [slider recursiveDescription]
<UISlider: 0x7fd7e3e43b60; frame = (0 0; 100 100); opaque = NO; layer = <CALayer: 0x7fd7e3e42460>; value: 0.000000>
   | <UIView: 0x7fd7e7077e20; frame = (16 49; 82 2); clipsToBounds = YES; opaque = NO; userInteractionEnabled = NO; layer = <CALayer: 0x7fd7e7077f30>>
   |    | <UIImageView: 0x7fd7e70765e0; frame = (-14 0; 96 2); clipsToBounds = YES; opaque = NO; userInteractionEnabled = NO; layer = <CALayer: 0x7fd7e7076310>>
   | <UIImageView: 0x7fd7e7077b40; frame = (2 49; 14 2); clipsToBounds = YES; opaque = NO; userInteractionEnabled = NO; layer = <CALayer: 0x7fd7e70767d0>>
   | <UIImageView: 0x7fd7e3d1d880; frame = (0 34; 31 31); opaque = NO; userInteractionEnabled = NO; layer = <CALayer: 0x7fd7e3d1d9c0>>
   |    | <UIImageView: 0x7fd7e3d1d400; frame = (-13 -6.5; 57 43.5); opaque = NO; userInteractionEnabled = NO; layer = <CALayer: 0x7fd7e3d1ba50>>

You can hide and unhide views in the debugger to see which views are responsible for what in the slider. One of the two point tall views will be the blue bar behind the slider thumb. The other will be the bar in front of the slider thumb. The 31x31 image view is the thumb. Anything you draw with drawRect: is going to go into the top level slider view itself (in the example, the view with address 0x7fd7e3e43b60). Then all the subviews draw on top of that.

Upvotes: 2

Related Questions