Search code examples
uicollectionviewautolayoutuicollectionviewcellcalayercashapelayer

iOS UICollectionViewCell resizable dashed border


I have a UICollectionView where some of the cells should have a dashed border and some of them should have a solid border. Also, the cells can be of varying size depending on the content that is present in the data model.

The problem I am having is that I cannot get the dashed border to be the same size as the collection view cell and again, the cell size can change based on the content. But basically, the cell should either have a dashed border or a solid border. The solid border is easy to get to resize to the correct size.

Here is a picture of what it looks like right now. The dashed border is colored green just to make it easier to see.

rendered view

Here is the view hierarchy debug view. There are two dashed borders here because I have been experimenting. The green border is a sublayer on the UICollectionViewCell's root layer. The grey border is a sublayer of a separate view that is a subview of the collection view cell's contentView property.

view debugger

Code

Approach 1 - add a dedicated view with a sublayer

Here I am trying to add a UIView subclass that has a dashed border. Then, when I need to show the dashed border or hide the dashed border, I just set the hidden property of the view accordingly. This works fine, except I cannot get the dashed border sublayer to resize.

The view is resizing to be the correct width and height based on the AutoLayout constraints, as can be seen in the view hierarchy debugger screenshot above. But the sublayer is still the original size (approximately 50px x 50px, which I guess is coming from the UICollectionView because I am not specifying that size anywhere).

For this implementation, I have a custom UIView subclass called MyResizableSublayerView. It overrides layoutSublayersOfLayer to handle the resizing of the sublayer, or at least that is what is supposed to be happening, but clearly it is not working.

But then the MyResizableSublayerView class is used in the collection view cell to add the dashed border to the view hierarchy.

MyResizableSublayerView
@interface MyResizableSublayerView : UIView

@property (strong, nonatomic) CAShapeLayer *borderLayer;

+ (instancetype)viewWithBorderSublayer:(CAShapeLayer *)shapeLayer;

@end




@implementation MyResizableSublayerView

+ (instancetype)viewWithBorderSublayer:(CAShapeLayer *)shapeLayer {
    CIResizableSublayerView *view = [[MyResizableSublayerView alloc] init];
    view.borderLayer = shapeLayer;
    return view;
}

- (void)setBorderLayer:(CAShapeLayer *)borderLayer {
    if (self->_borderLayer) {
        [self->_borderLayer removeFromSuperlayer];
    }
    
    self->_borderLayer = borderLayer;
    [self.layer addSublayer:self->_borderLayer];
}

- (void)layoutSublayersOfLayer:(CALayer *)layer {
    [super layoutSublayersOfLayer:layer];
    self.borderLayer.frame = layer.bounds;
}


@end
MyCollectionViewCell
@interface MyCollectionViewCell ()

@property (strong, nonatomic) MyResizableSublayerView *unavailableBorderView;

@end



@implementation MyCollectionViewCell

- (instancetype)init {
    return [self initWithFrame:CGRectZero];
}

- (instancetype)initWithCoder:(NSCoder *)coder {
    if (self = [super initWithCoder:coder]) {
        [self initialize];
    }
    return self;
}

- (instancetype)initWithFrame:(CGRect)frame {
    if (self = [super initWithFrame:frame]) {
        [self initialize];
    }
    return self;
}

- (void)initialize {
    self.contentView.layer.cornerRadius = 4.0f;
    self.contentView.layer.borderWidth = 1.0f;

    //... add other subviews
    [self.contentView addSubview:self.unavailableBorderView];
    
    [NSLayoutConstraint activateConstraints:@[
        [self.contentView.widthAnchor constraintLessThanOrEqualToConstant:250.0],
        [self.contentView.widthAnchor constraintGreaterThanOrEqualToConstant:100.0],
        
        [self.unavailableBorderView.leadingAnchor constraintEqualToAnchor:self.contentView.leadingAnchor],
        [self.unavailableBorderView.topAnchor constraintEqualToAnchor:self.contentView.topAnchor],
        [self.unavailableBorderView.trailingAnchor constraintEqualToAnchor:self.contentView.trailingAnchor],
        [self.unavailableBorderView.bottomAnchor constraintEqualToAnchor:self.contentView.bottomAnchor],
        
        //... constraints for other views
    ]];
}

- (MyResizableSublayerView *)unavailableBorderView {
    if (!self->_unavailableBorderView) {
        CAShapeLayer *layer = [CAShapeLayer layer];
        layer.strokeColor = [UIColor colorWithRed:0xE0/255.0 green:0xE0/255.0 blue:0xE0/255.0 alpha:1.0].CGColor;
        layer.lineWidth = 4.0;
        layer.lineJoin = kCALineJoinRound;
        layer.fillColor = [UIColor clearColor].CGColor;
        layer.lineDashPattern = @[@4, @4];
        layer.frame = self.contentView.bounds;
        layer.path = [UIBezierPath bezierPathWithRoundedRect:self.contentView.bounds cornerRadius:self.contentView.layer.cornerRadius].CGPath;

        self->_unavailableBorderView = [MyResizableSublayerView viewWithBorderSublayer:layer];
        self->_unavailableBorderView.translatesAutoresizingMaskIntoConstraints = NO;
        self->_unavailableBorderView.layer.cornerRadius = self.contentView.layer.cornerRadius;
        self->_unavailableBorderView.backgroundColor = [UIColor colorWithWhite:1.0 alpha:0.0];
    }
    return self->_unavailableBorderView;
}

//... more logic

@end

Approach 2 - add directly to the UICollectionViewCell

For this approach, I add the CAShapeLayer directly to the UICollectionViewCell and then override the layoutSublayersOfLayer to try to resize the dashed border sublayer, but this is not working either.

@interface MyCollectionViewCell ()

@property (strong, nonatomic) CAShapeLayer *unavailableBorderLayer;

@end



@implementation MyCollectionViewCell

- (instancetype)init {
    return [self initWithFrame:CGRectZero];
}

- (instancetype)initWithCoder:(NSCoder *)coder {
    if (self = [super initWithCoder:coder]) {
        [self initialize];
    }
    return self;
}

- (instancetype)initWithFrame:(CGRect)frame {
    if (self = [super initWithFrame:frame]) {
        [self initialize];
    }
    return self;
}

- (void)initialize {
    self.contentView.layer.cornerRadius = 4.0f;
    self.contentView.layer.borderWidth = 1.0f;

    //... add other subviews
    
    [NSLayoutConstraint activateConstraints:@[
        [self.contentView.widthAnchor constraintLessThanOrEqualToConstant:250.0],
        [self.contentView.widthAnchor constraintGreaterThanOrEqualToConstant:100.0],
        
        //... constraints for other views
    ]];
    
    
    CAShapeLayer *layer = [CAShapeLayer layer];
    layer.strokeColor = [UIColor greenColor].CGColor;
    layer.lineWidth = 2.0;
    layer.lineJoin = kCALineJoinRound;
    layer.fillColor = [UIColor clearColor].CGColor;
    layer.lineDashPattern = @[@4, @4];
    layer.frame = self.contentView.bounds;
    layer.path = [UIBezierPath bezierPathWithRoundedRect:self.contentView.bounds cornerRadius:self.contentView.layer.cornerRadius].CGPath;
    self->_unavailableBorderLayer = layer;
    [self.layer addSublayer:self->_unavailableBorderLayer];
}

- (void)layoutSublayersOfLayer:(CALayer *)layer {
    [super layoutSublayersOfLayer:layer];
    self.unavailableBorderLayer.frame = self.bounds;
}

//... more logic

@end

Questions

I have a couple of questions about this.

  1. What is wrong with my code that is not allowing the dashed border to resize to be the same size as the collection view cell?
  2. Which approach is the best approach to add a dashed border to the collection view cell. Or is there a better approach than the ones that I have listed here? Again my goal is to be able to show or hide the dashed border and for it to be the same size as the collection view cell, which is dynamically sized.

Solution

  • It's not quite clear what you're doing with constraints on the content view ... however, if you are getting the layout you want, except for the dashed borders, give this a try.

    First, instead of layoutSublayersOfLayer, use:

    - (void)layoutSubviews {
        [super layoutSubviews];
        _unavailableBorderLayer.frame = self.bounds;
    }