2

I would like to subclass UITextView to be able to draw round rect with custom border plus top+left inner shadow.

I had big success in creating this effect (by reading this post) for any (static bounds) view but had problems with scrolling views.

This effect I'm doing in -setFrame instance method of my custom class:

- (void)setFrame:(CGRect)frame {
[super setFrame:frame];

UIColor *borderColor = [UIColor colorWithRed:BORDER_COLOR_RED green:BORDER_COLOR_GREEN blue:BORDER_COLOR_BLUE alpha:BORDER_COLOR_ALPHA];

// Store index of the shadow sublayer for future use
[self setShadowLayerIndex:[LayerFormatter formatAsRoundRectWithShadowOn:self withBackgroundColor:[UIColor whiteColor] andBorderColor:borderColor]];}

formatAsRoundRectWithShadowOn: is a class method defined as:

+(NSUInteger)formatAsRoundRectWithShadowOn:(UIView*)view withBackgroundColor:(UIColor *)backgroundColor andBorderColor:(UIColor *)borderColor {
if([view isKindOfClass:[UITextField class]])
    ((UITextField*)view).borderStyle = UITextBorderStyleNone;
view.backgroundColor = backgroundColor;

view.layer.borderWidth = 1.0;
view.layer.borderColor = [borderColor CGColor];
view.layer.cornerRadius = CORNER_RADIUS;
view.layer.masksToBounds = YES;

//Add some insets to the text: https://stackoverflow.com/a/4423805
UIView *paddingView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 5, 20)];

if([view isKindOfClass:[UITextField class]])
{
    ((UITextField*)view).leftView = paddingView;
    ((UITextField*)view).leftViewMode = UITextFieldViewModeAlways;
}

// Create and apply a shadow layer. Help here: https://stackoverflow.com/questions/4431292/inner-shadow-effect-on-uiview-layer
CAShapeLayer* shadowLayer = [CAShapeLayer layer];

[shadowLayer setFrame:view.bounds];
[shadowLayer setShadowColor:[[UIColor blackColor] CGColor]];
[shadowLayer setShadowRadius:3.0f];
[shadowLayer setShadowOpacity:0.35f];
[shadowLayer setShadowOffset:CGSizeMake(2.0f, 2.0f)];

[shadowLayer setFillColor:[backgroundColor CGColor]];

// Causes the inner region in this example to NOT be filled.
[shadowLayer setFillRule:kCAFillRuleEvenOdd];

// Create inner and outer rectangle paths.
CGMutablePathRef path = CGPathCreateMutable();

// Outer path should be bigger than the field
UIBezierPath *bpOuter = [UIBezierPath bezierPathWithRect:CGRectInset(shadowLayer.bounds, -10, -10)];

// Inner path is the visible part of the view
UIBezierPath *bpInner = [UIBezierPath bezierPathWithRoundedRect:shadowLayer.bounds cornerRadius:CORNER_RADIUS];

// Add outer path and then add the inner path so it's subtracted from the outer path.
CGPathAddPath(path, NULL, bpOuter.CGPath);
CGPathAddPath(path, NULL, bpInner.CGPath);

CGPathCloseSubpath(path);

[shadowLayer setPath:path];
CGPathRelease(path);

[[view layer] addSublayer:shadowLayer];

NSUInteger addedAtIndex = [[[view layer] sublayers] indexOfObject:shadowLayer];
return addedAtIndex;}

To handle correct shadow+border display when the text view is scrolling i'm using my custom class -setBounds method to update the shadow layer frame:

-(void)setBounds:(CGRect)bounds{
[super setBounds:bounds];

// Change the frame of the shadow layer to reflect new bounds
[[[[self layer] sublayers] objectAtIndex:self.shadowLayerIndex] setFrame:bounds];}

The issue I'm experiencing is a incorrect shadow+frame drawing at the bottom when scrolling down (when inserting new lines of text) or at the top (when scrolling up to the beginning of the text).

After the new line is inserted or scroll completes (the view is again static) the view is drawn correct.

Any insight on the issue is more than welcome.

Community
  • 1
  • 1
kr45ko
  • 495
  • 1
  • 3
  • 13

1 Answers1

1

I had some time to dig into the problem and found a solution I want to share.

At first I realise that it's better to setup the shadow layer in -awakeFromNib instead in -setFrame.

To handle correct shadow+border display when the text view is scrolling I've changed the approach like this: to update the shadow layer I'm now using -layoutSubviews override in my custom class. In -layoutSubviews override I'm recreating the shadow layer to respect new bounds then calling [super layoutSubviews].

Scrolling, changing orientation - it works like a charm!

- (void)layoutSubviews
{
    [self updateShadow];
    [super layoutSubviews];
}

- (void)updateShadow
{
    if (shadowLayer)
        [shadowLayer removeFromSuperlayer];

    shadowLayer = [LayerFormatter addInnerShadowLayerOn:self withShadowColor:[UIColor blackColor]];
}   

Note that +(NSUInteger)formatAsRoundRectWithShadowOn:(UIView*)view withBackgroundColor:(UIColor *)backgroundColor andBorderColor:(UIColor *)borderColor is now returning layer reference to the already added layer in the view's layer hierarchy (the rework is simple):

+(CAShapeLayer *)addInnerShadowLayerOn:(UIView *)view withShadowColor:(UIColor *)shadowColor;

Any evidence/comments that this approach is correct and not just working would be highly appreciated.

kr45ko
  • 495
  • 1
  • 3
  • 13