7

I'm trying to add a hexagon mask to a UIImage which I has been successful in. However I am unable to round the sides of the hexagon mask. I thought adding cell.profilePic.layer.cornerRadius = 10; would do the trick but it hasn't.

Here is my code:

    CGRect rect = cell.profilePic.frame;

CAShapeLayer *hexagonMask = [CAShapeLayer layer];
CAShapeLayer *hexagonBorder = [CAShapeLayer layer];
hexagonBorder.frame = cell.profilePic.layer.bounds;
UIBezierPath *hexagonPath = [UIBezierPath bezierPath];
CGFloat sideWidth = 2 * ( 0.5 * rect.size.width / 2 );
CGFloat lcolumn = ( rect.size.width - sideWidth ) / 2;
CGFloat rcolumn = rect.size.width - lcolumn;
CGFloat height = 0.866025 * rect.size.height;
CGFloat y = (rect.size.height - height) / 2;
CGFloat by = rect.size.height - y;
CGFloat midy = rect.size.height / 2;
CGFloat rightmost = rect.size.width;
[hexagonPath moveToPoint:CGPointMake(lcolumn, y)];
[hexagonPath addLineToPoint:CGPointMake(rcolumn, y)];
[hexagonPath addLineToPoint:CGPointMake(rightmost, midy)];
[hexagonPath addLineToPoint:CGPointMake(rcolumn, by)];
[hexagonPath addLineToPoint:CGPointMake(lcolumn, by)];
[hexagonPath addLineToPoint:CGPointMake(0, midy)];
[hexagonPath addLineToPoint:CGPointMake(lcolumn, y)];

hexagonMask.path = hexagonPath.CGPath;
hexagonBorder.path = hexagonPath.CGPath;
hexagonBorder.fillColor = [UIColor clearColor].CGColor;
hexagonBorder.strokeColor = [UIColor blackColor].CGColor;
hexagonBorder.lineWidth = 5;
cell.profilePic.layer.mask = hexagonMask;
cell.profilePic.layer.cornerRadius = 10;
cell.profilePic.layer.masksToBounds = YES;
[cell.profilePic.layer addSublayer:hexagonBorder];

Any ideas?

Thanks

Khledon
  • 193
  • 5
  • 23
  • There is some useful and detailed math about rounding the corners of a polygon here: http://stackoverflow.com/questions/20442203/uibezierpath-triangle-with-rounded-edges – Zev Eisenberg Jul 15 '14 at 22:21

4 Answers4

58

You can define a path that is a hexagon with rounded corners (defining that path manually) and then apply that as the mask and border sublayer:

CGFloat lineWidth    = 5.0;
UIBezierPath *path   = [UIBezierPath polygonInRect:self.imageView.bounds
                                             sides:6
                                         lineWidth:lineWidth
                                      cornerRadius:30];

// mask for the image view

CAShapeLayer *mask   = [CAShapeLayer layer];
mask.path            = path.CGPath;
mask.lineWidth       = lineWidth;
mask.strokeColor     = [UIColor clearColor].CGColor;
mask.fillColor       = [UIColor whiteColor].CGColor;
self.imageView.layer.mask = mask;

// if you also want a border, add that as a separate layer

CAShapeLayer *border = [CAShapeLayer layer];
border.path          = path.CGPath;
border.lineWidth     = lineWidth;
border.strokeColor   = [UIColor blackColor].CGColor;
border.fillColor     = [UIColor clearColor].CGColor;
[self.imageView.layer addSublayer:border];

Where the path of a regular polygon with rounded corners might be implemented in a category like so:

@interface UIBezierPath (Polygon)

/** Create UIBezierPath for regular polygon with rounded corners
 *
 * @param rect          The CGRect of the square in which the path should be created.
 * @param sides         How many sides to the polygon (e.g. 6=hexagon; 8=octagon, etc.).
 * @param lineWidth     The width of the stroke around the polygon. The polygon will be inset such that the stroke stays within the above square.
 * @param cornerRadius  The radius to be applied when rounding the corners.
 *
 * @return              UIBezierPath of the resulting rounded polygon path.
 */

+ (instancetype)polygonInRect:(CGRect)rect sides:(NSInteger)sides lineWidth:(CGFloat)lineWidth cornerRadius:(CGFloat)cornerRadius;

@end

And

@implementation UIBezierPath (Polygon)

+ (instancetype)polygonInRect:(CGRect)rect sides:(NSInteger)sides lineWidth:(CGFloat)lineWidth cornerRadius:(CGFloat)cornerRadius {
    UIBezierPath *path  = [UIBezierPath bezierPath];

    CGFloat theta       = 2.0 * M_PI / sides;                           // how much to turn at every corner
    CGFloat offset      = cornerRadius * tanf(theta / 2.0);             // offset from which to start rounding corners
    CGFloat squareWidth = MIN(rect.size.width, rect.size.height);       // width of the square

    // calculate the length of the sides of the polygon

    CGFloat length      = squareWidth - lineWidth;
    if (sides % 4 != 0) {                                               // if not dealing with polygon which will be square with all sides ...
        length = length * cosf(theta / 2.0) + offset/2.0;               // ... offset it inside a circle inside the square
    }
    CGFloat sideLength = length * tanf(theta / 2.0);

    // start drawing at `point` in lower right corner

    CGPoint point = CGPointMake(rect.origin.x + rect.size.width / 2.0 + sideLength / 2.0 - offset, rect.origin.y + rect.size.height / 2.0 + length / 2.0);
    CGFloat angle = M_PI;
    [path moveToPoint:point];

    // draw the sides and rounded corners of the polygon

    for (NSInteger side = 0; side < sides; side++) {
        point = CGPointMake(point.x + (sideLength - offset * 2.0) * cosf(angle), point.y + (sideLength - offset * 2.0) * sinf(angle));
        [path addLineToPoint:point];

        CGPoint center = CGPointMake(point.x + cornerRadius * cosf(angle + M_PI_2), point.y + cornerRadius * sinf(angle + M_PI_2));
        [path addArcWithCenter:center radius:cornerRadius startAngle:angle - M_PI_2 endAngle:angle + theta - M_PI_2 clockwise:YES];

        point = path.currentPoint; // we don't have to calculate where the arc ended ... UIBezierPath did that for us
        angle += theta;
    }

    [path closePath];

    path.lineWidth = lineWidth;           // in case we're going to use CoreGraphics to stroke path, rather than CAShapeLayer
    path.lineJoinStyle = kCGLineJoinRound;

    return path;
}

That yields something like so:

landscape with rounded hexagon border

Or in Swift 3 you might do:

let lineWidth: CGFloat = 5
let path = UIBezierPath(polygonIn: imageView.bounds, sides: 6, lineWidth: lineWidth, cornerRadius: 30)

let mask = CAShapeLayer()
mask.path            = path.cgPath
mask.lineWidth       = lineWidth
mask.strokeColor     = UIColor.clear.cgColor
mask.fillColor       = UIColor.white.cgColor
imageView.layer.mask = mask

let border = CAShapeLayer()
border.path          = path.cgPath
border.lineWidth     = lineWidth
border.strokeColor   = UIColor.black.cgColor
border.fillColor     = UIColor.clear.cgColor
imageView.layer.addSublayer(border)

With

extension UIBezierPath {

    /// Create UIBezierPath for regular polygon with rounded corners
    ///
    /// - parameter rect:            The CGRect of the square in which the path should be created.
    /// - parameter sides:           How many sides to the polygon (e.g. 6=hexagon; 8=octagon, etc.).
    /// - parameter lineWidth:       The width of the stroke around the polygon. The polygon will be inset such that the stroke stays within the above square. Default value 1.
    /// - parameter cornerRadius:    The radius to be applied when rounding the corners. Default value 0.

    convenience init(polygonIn rect: CGRect, sides: Int, lineWidth: CGFloat = 1, cornerRadius: CGFloat = 0) {
        self.init()

        let theta = 2 * .pi / CGFloat(sides)                 // how much to turn at every corner
        let offset = cornerRadius * tan(theta / 2)           // offset from which to start rounding corners
        let squareWidth = min(rect.width, rect.height)       // width of the square

        // calculate the length of the sides of the polygon

        var length = squareWidth - lineWidth
        if sides % 4 != 0 {                                  // if not dealing with polygon which will be square with all sides ...
            length = length * cos(theta / 2) + offset / 2    // ... offset it inside a circle inside the square
        }
        let sideLength = length * tan(theta / 2)

        // if you'd like to start rotated 90 degrees, use these lines instead of the following two:
        //
        // var point = CGPoint(x: rect.midX - length / 2, y: rect.midY + sideLength / 2 - offset)
        // var angle = -CGFloat.pi / 2.0

        // if you'd like to start rotated 180 degrees, use these lines instead of the following two:
        //
        // var point = CGPoint(x: rect.midX - sideLength / 2 + offset, y: rect.midY - length / 2)
        // var angle = CGFloat(0)

        var point = CGPoint(x: rect.midX + sideLength / 2 - offset, y: rect.midY + length / 2)
        var angle = CGFloat.pi

        move(to: point)

        // draw the sides and rounded corners of the polygon

        for _ in 0 ..< sides {
            point = CGPoint(x: point.x + (sideLength - offset * 2) * cos(angle), y: point.y + (sideLength - offset * 2) * sin(angle))
            addLine(to: point)

            let center = CGPoint(x: point.x + cornerRadius * cos(angle + .pi / 2), y: point.y + cornerRadius * sin(angle + .pi / 2))
            addArc(withCenter: center, radius: cornerRadius, startAngle: angle - .pi / 2, endAngle: angle + theta - .pi / 2, clockwise: true)

            point = currentPoint
            angle += theta
        }

        close()

        self.lineWidth = lineWidth           // in case we're going to use CoreGraphics to stroke path, rather than CAShapeLayer
        lineJoinStyle = .round
    }

}

For Swift 2 rendition, see previous revision of this answer.

Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • Lol. The polygon routine is generalized, so you can supply whatever number of sides you want. I've updated the sample accordingly. – Rob Jul 16 '14 at 19:03
  • 2
    I wasn't making a bad comment and didn't even see the sides parameter. This is a great answer. – Desdenova Jul 16 '14 at 20:04
  • @Rob thanks for nice code but i can't set the image using that layer i am using this code for it mask.contents = (id)[[UIImage imageNamed:@"user_profile.png"] CGImage]; – Rushabh Sep 09 '14 at 13:17
  • If you're trying to mask an image, you set the image view's `image` property (not the mask's `contents` property) and then apply the mask to be the `CAShapeLayer`. It you want to mask an image using the alpha channel of another image, then you can use that `contents` property of the mask layer (but then you presumably don't need any of this complicated `CAShapeLayer` stuff). If this is unclear, I'd suggest you post you own question and maybe we can help you out more. – Rob Sep 09 '14 at 13:34
  • @Rob thanks for replay i want to do same as u have seen yr answer image.how can u set the image in hexagon using your code. i am using the CALayer to set the image but i can't set image in hexagon.this is my last comment then after i post question on StackOverflow. – Rushabh Sep 12 '14 at 06:04
  • You're over thinking this. It's a simple `UIImageView`, so create your `UIImageView` and set its `image`, like usual. Then, once you have your image showing up in your `UIImageView`, set the `mask` of the `layer` of the `UIImageView`, as shown above. – Rob Sep 12 '14 at 09:21
  • Love this solution, thank you! FYI, with lineWidth set to 0.0 a black border still appears. Easy enough to accommodate this case. For example: if(borderWidth == 0.0) { border.strokeColor = [UIColor clearColor].CGColor; } else { border.strokeColor = [UIColor blackColor].CGColor; } – 0xMatthewGroves Apr 22 '15 at 04:38
  • 1
    @FrostRocket - In the code samples above, I'm adding a layer for the border and then applying a mask. If you don't want a border, rather than stroking a clear border, just don't add that sublayer at all. Just set the mask and you're done. No border. – Rob Oct 07 '16 at 23:19
  • two border draw when i scroll tableview – Shahbaz Akram Jun 12 '17 at 07:48
  • It's impossible to diagnose that problem on the basis of those seven words alone. @MianShahbazAkram - I'd suggest you (a) delete the two above comments; and (b) that you post your own question with reproducible example of the problem](http://stackoverflow.com/help/mcve), showing us what you tried, a screen snapshot of what it rendered, and a more clear explanation of what you intended (if not otherwise obvious at that point). – Rob Jun 12 '17 at 09:46
  • How to rotate the same to .pi/2 ?. I tried changing the angle but its failing. – Sarath Neeravallil Feb 16 '18 at 18:35
  • @SarathNeeravallil - I've added comment in Swift code showing how to rotate it .pi / 2. – Rob Feb 17 '18 at 06:28
  • @Rob I get the feeling that the first part of the answer creates layers on top of layers when applied as a method to a UIImageView, which is embeded in a UICollectionCell. Say I apply, `imgToPolygon.createPolygon(color: red)` and then to remove the strokeColor `imgToPolygon.createPolygon(color: clear)` => red persists as strokeColor of the border. My workaround is to use `imgToPolygon.createPolygon(color: white)` since my background layer is white. This feel like a terrible hacks, tho. Sorry, I didn't want to start a new question for this minor detail and it could be helpful for others. – Ando Jul 20 '18 at 19:20
  • @Rob perfect but what if I want pentagon upside down? – Bhavin Bhadani Aug 10 '18 at 05:33
  • 1
    @EICaptainv2.0 - Just change starting `point` and `angle`. See added comment in the revised Swift example, above. – Rob Aug 11 '18 at 18:02
  • @Rob Thanks. It's been a great help :) – Bhavin Bhadani Aug 13 '18 at 04:13
  • 1
    Woah woah woah! This works amazing! Thank you! – Ximena Flores de la Tijera Oct 07 '21 at 17:19
  • Is it possible to add a gradient color to the rounded corners of this hexagon? – Ximena Flores de la Tijera Oct 07 '21 at 17:33
  • It depends upon your intent. If you want a single linear gradient for the entire path (e.g., from left to right or top to bottom), that's pretty easy. If you wanted a gradient from the inner part of the path to the outer portion, that's more complicated, but can be done. But all of this is beyond the scope of this question, so I'd suggest posting your own question. But I'd suggest [you search about gradients and paths](https://stackoverflow.com/search?q=%5Bios%5D+gradient+to+path) before posting a duplicate question. – Rob Oct 09 '21 at 20:53
4

Here is the swift 3 version of Rob's answer.

    let lineWidth: CGFloat = 5.0
    let path = UIBezierPath(roundedPolygonPathWithRect: self.bounds, lineWidth: lineWidth, sides: 6, cornerRadius: 12)

    let mask = CAShapeLayer()
    mask.path = path.cgPath
    mask.lineWidth = lineWidth
    mask.strokeColor = UIColor.clear.cgColor
    mask.fillColor = UIColor.white.cgColor
    self.layer.mask = mask

    let border = CAShapeLayer()
    border.path = path.cgPath
    border.lineWidth = lineWidth
    border.strokeColor = UIColor.black.cgColor
    border.fillColor = UIColor.clear.cgColor
    self.layer.addSublayer(border)

extension UIBezierPath {

    convenience init(roundedPolygonPathWithRect rect: CGRect, lineWidth: CGFloat, sides: NSInteger, cornerRadius: CGFloat) {

        self.init()

        let theta = CGFloat(2.0 * M_PI) / CGFloat(sides)
        let offSet = CGFloat(cornerRadius) / CGFloat(tan(theta/2.0))
        let squareWidth = min(rect.size.width, rect.size.height)

        var length = squareWidth - lineWidth

        if sides%4 != 0 {
            length = length * CGFloat(cos(theta / 2.0)) + offSet/2.0
        }
        let sideLength = length * CGFloat(tan(theta / 2.0))

        var point = CGPoint(x: squareWidth / 2.0 + sideLength / 2.0 - offSet, y: squareWidth - (squareWidth - length) / 2.0)
        var angle = CGFloat(M_PI)
        move(to: point)

        for _ in 0 ..< sides {
            point = CGPoint(x: point.x + CGFloat(sideLength - offSet * 2.0) * CGFloat(cos(angle)), y: point.y + CGFloat(sideLength - offSet * 2.0) * CGFloat(sin(angle)))
            addLine(to: point)

            let center = CGPoint(x: point.x + cornerRadius * CGFloat(cos(angle + CGFloat(M_PI_2))), y: point.y + cornerRadius * CGFloat(sin(angle + CGFloat(M_PI_2))))
            addArc(withCenter: center, radius:CGFloat(cornerRadius), startAngle:angle - CGFloat(M_PI_2), endAngle:angle + theta - CGFloat(M_PI_2), clockwise:true)

            point = currentPoint // we don't have to calculate where the arc ended ... UIBezierPath did that for us
            angle += theta
        }

        close()
    }
}
Community
  • 1
  • 1
Vikas
  • 666
  • 5
  • 15
3

Here's a conversion of the roundedPolygon method for Swift.

func roundedPolygonPathWithRect(square: CGRect, lineWidth: Float, sides: Int, cornerRadius: Float) -> UIBezierPath {
    var path = UIBezierPath()

    let theta = Float(2.0 * M_PI) / Float(sides)
    let offset = cornerRadius * tanf(theta / 2.0)
    let squareWidth = Float(min(square.size.width, square.size.height))

    var length = squareWidth - lineWidth

    if sides % 4 != 0 {
        length = length * cosf(theta / 2.0) + offset / 2.0
    }

    var sideLength = length * tanf(theta / 2.0)

    var point = CGPointMake(CGFloat((squareWidth / 2.0) + (sideLength / 2.0) - offset), CGFloat(squareWidth - (squareWidth - length) / 2.0))
    var angle = Float(M_PI)
    path.moveToPoint(point)

    for var side = 0; side < sides; side++ {

        let x = Float(point.x) + (sideLength - offset * 2.0) * cosf(angle)
        let y = Float(point.y) + (sideLength - offset * 2.0) * sinf(angle)

        point = CGPointMake(CGFloat(x), CGFloat(y))
        path.addLineToPoint(point)

        let centerX = Float(point.x) + cornerRadius * cosf(angle + Float(M_PI_2))
        let centerY = Float(point.y) + cornerRadius * sinf(angle + Float(M_PI_2))

        var center = CGPointMake(CGFloat(centerX), CGFloat(centerY))

        let startAngle = CGFloat(angle) - CGFloat(M_PI_2)
        let endAngle = CGFloat(angle) + CGFloat(theta) - CGFloat(M_PI_2)

        path.addArcWithCenter(center, radius: CGFloat(cornerRadius), startAngle: startAngle, endAngle: endAngle, clockwise: true)

        point = path.currentPoint
        angle += theta
    }

    path.closePath()

    return path
}
Snwspeckle
  • 105
  • 2
  • 9
-1

Maybe you have to round the corners of the border and the mask, rather than of the image view?

hexagonMask.cornerRadius = hexagonBorder.cornerRadius = 10.0;
Mundi
  • 79,884
  • 17
  • 117
  • 140
  • 1
    This won’t work. It only rounds the square corners of the layer as described by its `frame` property. You need to round the actual corners of the hexagon. – Zev Eisenberg Jul 15 '14 at 22:20