CALayer Partial Corner Radii

Johan Attali bio photo By Johan Attali

I love using CALayers. The simple fact that almost all of their properties are automatically animated makes them a great companion to build responsive UI in applications.

CALayers also come with a lot of cool features like cornerRadius, probably something you have used many times. But once in while you would like to have rounded corners for only part of your layer, instead of all four corners.

While looking for a way to achieve this, I found this answer on StackOverflow:

UIBezierPath *maskPath;
maskPath = [UIBezierPath bezierPathWithRoundedRect:_backgroundImageView.bounds
                                 byRoundingCorners:(UIRectCornerBottomLeft|UIRectCornerBottomRight)
                                       cornerRadii:CGSizeMake(3.0, 3.0)];

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

This is nice but you if your layer changes size you have to run through that code again in order to update the mask.

I thought there could be a much nicer way to implement this, especially with Swift by creating an extension on CALayer and using a computed property.

The first thing consists of emulating the previous code in swift. This can be easily done with a closure:

let closure:((Void)->Void) = {
  let path = UIBezierPath(roundedRect: self.bounds, byRoundingCorners: v.corners, cornerRadii: CGSize(width: radius, height: radius))
  let mask = CAShapeLayer()
  mask.path = path.CGPath
  self.mask = mask
}

Next, and the most important thing to do is to swizzle the layoutSublayers method to make sure that closure is always applied when the layer is resized. Swizzling, is really something we should refrain to do, but in that case there's simple no choice. Luckily, Peter Steinberg (@steipete a great developer who keeps patching UIKit) wrote a great lib, Aspects, that greatly simplify the process of swizzling. I highly recommend you check it out.

But this lib uses native objects as blocks which means our closure has to be converted into an AnyObject type. Again, luckily Joe Groff, Apple Dev working on the swift compiler, has a solution:

let block: @convention(block) Void -> Void = closure
let objectBlock = unsafeBitCast(block, AnyObject.self)

The final step is to store the new properties through the usage of the objc runtime using objc_getAssociatedObject and objc_setAssociatedObject. Note that with swift 2.0, Apple moved the objc_AssociationPolicy into an enum type which is cool.

The whole extension looks like this:

extension CALayer {
  // This will hold the keys for the runtime property associations
  private struct AssociationKey {
    static var CornerRect:Int8 = 1    // for the UIRectCorner argument
    static var CornerRadius:Int8 = 2  // for the radius argument
  }

  // new computed property on CALayer
  // You send the corners you want to round (ex. [.TopLeft, .BottomLeft])
  // and the radius at which you want the corners to be round
  var cornerRadii:(corners: UIRectCorner, radius:CGFloat) {
    get {
      let number = objc_getAssociatedObject(self, &AssociationKey.CornerRect)  as? NSNumber ?? 0
      let radius = objc_getAssociatedObject(self, &AssociationKey.CornerRadius)  as? NSNumber ?? 0
      return (corners: UIRectCorner(rawValue: number.unsignedLongValue), radius: CGFloat(radius.floatValue))
    }
    set (v) {
      let radius = v.radius
      let closure:((Void)->Void) = {
        let path = UIBezierPath(roundedRect: self.bounds, byRoundingCorners: v.corners, cornerRadii: CGSize(width: radius, height: radius))
        let mask = CAShapeLayer()
        mask.path = path.CGPath
        self.mask = mask
      }
      let block: @convention(block) Void -> Void = closure
      let objectBlock = unsafeBitCast(block, AnyObject.self)
      objc_setAssociatedObject(self, &AssociationKey.CornerRect, NSNumber(unsignedLong: v.corners.rawValue), .OBJC_ASSOCIATION_RETAIN)
      objc_setAssociatedObject(self, &AssociationKey.CornerRadius, NSNumber(float: Float(v.radius)), .OBJC_ASSOCIATION_RETAIN)
      do { try aspect_hookSelector("layoutSublayers", withOptions: .PositionAfter, usingBlock: objectBlock) }
      catch _ { }
    }
  }
}