Selective layer borders

I always though that the most annoying problems are the ones that should have been trivial. Those pesky problems that happen all the time, and for some reason apple didn’t take the time to fix them.

One such problem is the fact that there is no easy way to draw “selective” borders – i.e draw borders only on the left and top side of the view.

This problem keeps popping up for me, the last time was when I was trying to draw a grid view with custom cells. I wanted the cells to have a 1px border, but since all cell had both top and bottom borders, the borders seemed to be 2px wide.

This time I decided to solve this problem the right way and make a simple drop-in class that provides flexible border abilities – something that I could use like this:


myView.borderDirection = AUIFlexibleBordersDirectionRight | AUIFlexibleBordersDirectionTop;

This turned out to be a little trickier than I expected.
My first attempt was to subclass CALayer, and override DrawInContext to draw the selective borders. This seemed like the most efficient and easy solution – the only problem was that it didn’t work.
It turns out that implementing DrawInContext, does not automatically cause drawing to occur. A quick reference in Apple documentation revealed
this annoying fact:

Subclassing CALayer and implementing one of the drawing methods does not automatically cause drawing to occur. You must explicitly cause the instance to re-cache the content, either by sending it a setNeedsDisplay or setNeedsDisplayInRect: message, or by setting its needsDisplayOnBoundsChange property to YES.

Well, obviously I wanted my solution to be a little more generic and manually calling setNeedsDisplay was just not good enough.

So, I decided to compromise and instead of custom drawing – simply add a sublayer that will draw the selective borders.

The implementation is pretty straight forward, so lets go over it step by step:

Subclassing CALayer

enum {
    AUISelectiveBordersFlagLeft = 1 <<  0,
    AUISelectiveBordersFlagRight = 1 <<  1, 
    AUISelectiveBordersFlagTop = 1 <<  2, 
    AUISelectiveBordersFlagBottom = 1 <<  3
};
typedef NSUInteger AUISelectiveBordersFlag;


@interface AUISelectiveBordersLayer : CALayer {
    CAShapeLayer *borderLayer;
}

@property (nonatomic, strong) UIColor *selectiveBordersColor;
@property (nonatomic) float selectiveBordersWidth;
@property (nonatomic) AUISelectiveBordersFlag selectiveBorderFlag;

@end

The first thing we did was defining a convenient enum to declare which borders we want, and then we simply subclass CALayer and add our custom properties that will be used to customize the borders

Adding the borders subLayer

-(void) setSelectiveBorderFlag:(AUISelectiveBordersFlag)newSelectiveBorderFlag
{
    if (!borderLayer) {
        borderLayer = [[CAShapeLayer alloc] init];
        [self addSublayer:borderLayer];
    }
    
    selectiveBorderFlag = newSelectiveBorderFlag;
    
    [self reloadBorders];
}

To avoid waste, we lazily create the border layer only if we’re setting the selective borders property.
The bulk of the solution is in the reloadBorders method:

-(void) reloadBorders
{
    if (!borderLayer)
        return;
    
    UIBezierPath *path = [[UIBezierPath alloc] init];
    if (selectiveBorderFlag & AUISelectiveBordersFlagLeft) { // left border
        CGPoint startPoint = CGPointMake(0-selectiveBordersWidth/2, 0);
        CGPoint endPoint = CGPointMake(0-selectiveBordersWidth/2, CGRectGetMaxY(self.bounds));
        [path moveToPoint:startPoint];
        [path addLineToPoint:endPoint];
    }
    if (selectiveBorderFlag & AUISelectiveBordersFlagRight) { // right border
        CGPoint startPoint = CGPointMake(CGRectGetMaxX(self.bounds)-selectiveBordersWidth/2, 0);
        CGPoint endPoint = CGPointMake(CGRectGetMaxX(self.bounds)-selectiveBordersWidth/2, CGRectGetMaxY(self.bounds));
        [path moveToPoint:startPoint];
        [path addLineToPoint:endPoint];
    }
    if (selectiveBorderFlag & AUISelectiveBordersFlagTop) { // top border
        CGPoint startPoint = CGPointMake(0, 0+selectiveBordersWidth/2);
        CGPoint endPoint = CGPointMake(CGRectGetMaxX(self.bounds), 0+selectiveBordersWidth/2);
        [path moveToPoint:startPoint];
        [path addLineToPoint:endPoint];
    }
    if (selectiveBorderFlag & AUISelectiveBordersFlagBottom) { // bottom border
        CGPoint startPoint = CGPointMake(0, CGRectGetMaxY(self.bounds)-selectiveBordersWidth/2);
        CGPoint endPoint = CGPointMake(CGRectGetMaxX(self.bounds), CGRectGetMaxY(self.bounds)-selectiveBordersWidth/2);
        [path moveToPoint:startPoint];
        [path addLineToPoint:endPoint];
    }
    
    borderLayer.frame = self.bounds;
    borderLayer.path = path.CGPath;
    
    borderLayer.strokeColor = self.selectiveBordersColor.CGColor;
    borderLayer.lineWidth = self.selectiveBordersWidth;
}

In this method we create a UIBezierPath that will draw lines according to the bitwise property selectiveBorderFlag. If you never used a UIBezierPath before, than I recommend checking it out, it’s a very useful class that lets you define a path consisting of straight and curved line segments and render that path in your custom views.

We’re almost finished, but there’s just one big point to pay attention to. We want to make sure that our borders are always above the content, but if we’re adding a subclass to our layer, that it will be added above all previous sublayer. To handle this we have to make sure that every time a layer is added, we move our borders layer back up. We do this in the layoutSublayers method:

Handling layout changes

-(void) layoutSublayers {
    [super layoutSublayers];
    [self reloadBorders];
    // Move the borders to the top
    borderLayer.zPosition = self.sublayers.count;
}

Note that we’re not only moving the borderLayer to the top, but we’re also calling reloadBorders again. This is because layoutSublayers is also called if the frame of the layer changes, and in this case we need to recalculate the borders path.

That’s it folks. You can download AUISelectiveBordersView from github and start using it in your projects.

Enjoy

3 Responses to Selective layer borders

  1. Niko Bertele says:

    Nice piece of work. 🙂
    Was really helpful for me, as i needed 5 buttons in a row each separated by a one pixel border.

  2. hvanbrug says:

    Nice work, but … there is a bug. When drawing the left line you want to be doing (0 + width / 2) instead of (0 – width / 2). Otherwise, seems to work well. 🙂

  3. Mark says:

    Nice work! Just what I needed.

    Small suggestion: It would be cool if you would be able to set default color and width.

    Then you would only have to set the “AUISelectiveBordersFlag” property.

    If I find the time, I might send a pull request on Github..

Leave a comment