Stroke masked CALayer in iOS
Asked Answered
D

2

13

I'm trying to create a label (or any other view for that matter) with one rounded corner and a stroke/border. I can achieve the former using the following code:

UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:self.label.bounds
                                      byRoundingCorners:UIRectCornerBottomRight
                                            cornerRadii:CGSizeMake(16.0f, 16.0f)];

CAShapeLayer *shape = [CAShapeLayer layer];
shape.frame = self.label.bounds;
shape.path = maskPath.CGPath;

self.label.layer.mask = shape;

Which works great for the rounded corner, but using the following code doesn't apply the stroke the way I want. Instead producing a black (or whatever the backgroundColor of self.label is set to) square border.

UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:self.label.bounds
                                      byRoundingCorners:UIRectCornerBottomRight
                                            cornerRadii:CGSizeMake(16.0f, 16.0f)];

CAShapeLayer *shape = [CAShapeLayer layer];
shape.frame = self.label.bounds;
shape.path = maskPath.CGPath;

// Add stroke
shape.borderWidth = 1.0f;
shape.borderColor = [UIColor whiteColor].CGColor;

self.label.backgroundColor = [UIColor blackColor];
self.label.layer.mask = shape;

Any suggestions on how I can apply an arbitrary coloured stroke that follows the masked path?

Deterioration answered 21/8, 2013 at 12:13 Comment(0)
N
38

You're on the right track with the shape layer. But you should have two different layers. First the mask layer as in your first example which masks your view (cuts off areas you don't want to be visible)

Then you add the shape layer too, but not as mask layer. Also, make sure to not use borderWidth and borderColor, but stroke.

//
// Create your mask first
//
UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:self.label.bounds
                                      byRoundingCorners:UIRectCornerBottomRight
                                            cornerRadii:CGSizeMake(16.0f, 16.0f)];

CAShapeLayer *maskLayer = [CAShapeLayer layer];
maskLayer.frame = self.label.bounds;
maskLayer.path = maskPath.CGPath;
self.label.layer.mask = maskLayer;    

//
// And then create the outline layer
//
CAShapeLayer *shape = [CAShapeLayer layer];
shape.frame = self.label.bounds;
shape.path = maskPath.CGPath;
shape.lineWidth = 3.0f;
shape.strokeColor = [UIColor whiteColor].CGColor;
shape.fillColor = [UIColor clearColor].CGColor;
[self.label.layer addSublayer:shape];

Be aware that your stroke layer path should be inside (smaller than) the the path of the mask. Otherwise, the stroked path will be masked away by the mask layer. I've set the lineWith to 3 which makes it so you can see half of the width (1.5 px), and the other half will be outside the mask.

Nobody answered 21/8, 2013 at 12:36 Comment(3)
That sort of works but now the text of the label has been covered by the shape layer :(Deterioration
OK I've managed to get it working by applying the mask to a UIView and adding the UILabel as a subview. Thanks very much :)Deterioration
StrokeLayer covers the label so i added this to fix that up shape.fillColor = [UIColor clearColor].CGColorPashalik
E
0

If you subclass CALayer, you could instantiate it with the mask you want, and also override layoutSubLayers to include the border you want on that mask.

Could do this a couple ways. Below Ill do it by using the path of the given mask, and assigning that to class property to be used for constructing the new border in layoutSubLayers. There is potential that this method could be called multiple times, so I also set a boolean to track this. (Could also assign the border as a class property, and remove/re-add each time. For now, I use bool check.

Swift 3:

class CustomLayer: CALayer {

    private var path: CGPath?
    private var borderSet: Bool = false

    init(maskLayer: CAShapeLayer) {
        super.init()
        self.path = maskLayer.path
        self.frame = maskLayer.frame
        self.bounds = maskLayer.bounds
        self.mask = maskLayer
    }

    override func layoutSublayers() {

        let newBorder = CAShapeLayer()

        newBorder.lineWidth = 12
        newBorder.path = self.path
        newBorder.strokeColor = UIColor.black.cgColor
        newBorder.fillColor = nil


        if(!borderSet) {
            self.addSublayer(newBorder)
            self.borderSet = true
        }

    }

    required override init(layer: Any) {
        super.init(layer: layer)
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}
Entoblast answered 23/8, 2017 at 21:58 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.