3

This is my attempt:

func round() {
    let width = bounds.width < bounds.height ? bounds.width : bounds.height
    let mask = CAShapeLayer()
    mask.path = UIBezierPath(ovalInRect: CGRectMake(bounds.midX - width / 2, bounds.midY - width / 2, width, width)).CGPath

    self.layer.mask = mask

    // add border
    let frameLayer = CAShapeLayer()
    frameLayer.path = mask.path
    frameLayer.lineWidth = 4.0
    frameLayer.strokeColor = UIColor.whiteColor().CGColor
    frameLayer.fillColor = nil

    self.layer.addSublayer(frameLayer)
}

It works on the iphone 6 simulator (storyboard has size of 4.7), but on the 5s and 6+ it looks weird:

enter image description here

enter image description here

Is this an auto layout issue? Without the border, auto layout works correct. This is my first time working with masks and so I am not sure if what I have done is correct.

round function is called in viewDidLayoutSubviews.

Any thoughts?

Dave
  • 498
  • 2
  • 7
  • 22
Adrian
  • 19,440
  • 34
  • 112
  • 219
  • 1
    Read the description of `viewDidLayoutSubviews`, it tells when the method is going to be called and it doesn't look to be perfect for what you want to achieve. – A-Live Aug 24 '15 at 14:29
  • And what is my alternative ? – Adrian Aug 24 '15 at 14:31
  • 1
    Make a subclass of the view class, store a reference to your added path and layer, modify them when the size changes are detected (use `layoutSubviews` for that), reuse it in the future. – A-Live Aug 24 '15 at 14:39
  • can you give me an example ? I am new at this. – Adrian Aug 24 '15 at 15:45

2 Answers2

17

If you have subclassed UIImageView, for example, you can override layoutSubviews such that it (a) updates the mask; (b) removes any old border; and (c) adds a new border. In Swift 3:

import UIKit

@IBDesignable
class RoundedImageView: UIImageView {

    /// saved rendition of border layer

    private weak var borderLayer: CAShapeLayer?

    override func layoutSubviews() {
        super.layoutSubviews()

        // create path

        let width = min(bounds.width, bounds.height)
        let path = UIBezierPath(arcCenter: CGPoint(x: bounds.midX, y: bounds.midY), radius: width / 2, startAngle: 0, endAngle: .pi * 2, clockwise: true)

        // update mask and save for future reference

        let mask = CAShapeLayer()
        mask.path = path.cgPath
        layer.mask = mask

        // create border layer

        let frameLayer = CAShapeLayer()
        frameLayer.path = path.cgPath
        frameLayer.lineWidth = 32.0
        frameLayer.strokeColor = UIColor.white.cgColor
        frameLayer.fillColor = nil

        // if we had previous border remove it, add new one, and save reference to new one

        borderLayer?.removeFromSuperlayer()
        layer.addSublayer(frameLayer)
        borderLayer = frameLayer
    }
}

That way, it responds to changing of the layout, but it makes sure to clean up any old borders.

By the way, if you are not subclassing UIImageView, but rather are putting this logic inside the view controller, you would override viewWillLayoutSubviews instead of layoutSubviews of UIView. But the basic idea would be the same.

--

By the way, I use a mask in conjunction with this shape layer because if you merely apply rounded corners of a UIView, it can result in weird artifacts (look at very thin gray line at lower part of the circular border):

artifact

If you use the bezier path approach, no such artifacts result:

no artifact

For Swift 2.3 example, see earlier revision of this answer.

Community
  • 1
  • 1
Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • 1
    What a great answer. This fixes all of my problems. Much appreciate it. – Adrian Aug 24 '15 at 17:30
  • Worked smoothly but can't we do this from draw method? – Nikhil Manapure Mar 20 '17 at 09:53
  • 1
    Wow. Great answer. So many similar questions on SA that fail to solve this problem. – Michael Ramos Aug 23 '17 at 19:43
  • @NikhilManapure - Yes you can manually mask and render in the [`draw(_:)`](https://developer.apple.com/documentation/uikit/uiview/1622529-draw) method, but we generally avoid that because we theoretically lose any optimizations/features that Apple’s implemented. Plus, this is a general mechanism that you can use regardless of the type of view or whatever subviews it might have. – Rob Mar 22 '20 at 15:55
4

The easiest way is to manipulate the layer of the image view itself.

imageView.layer.cornerRadius = imageView.bounds.size.width / 2.0
imageView.layer.borderWidth = 2.0
imageView.layer.borderColor = UIColor.whiteColor.CGColor
imageView.layer.masksToBounds = true

You can include this in viewDidLayoutSubviews or layoutSubviews to make sure the size is always correct.

NB: Maybe this technique makes your circle mask obsolete ;-). As a rule of thumb, always choose the simplest solution.

Mundi
  • 79,884
  • 17
  • 117
  • 140
  • I was using this, but to much problem with auto layout – Adrian Aug 24 '15 at 14:31
  • 1
    So you should be using this and solve your auto layout issues. – Mundi Aug 24 '15 at 14:58
  • 2
    I'd add `imageView.layer.masksToBounds = true`. – NRitH Aug 24 '15 at 15:14
  • I ment I was using this solution with cornerRadius, also setting masktoBounds = true, but my image needs to resize when running on different screens and had many issues with this, that I couldn't resolve them, so I said I should try masking. – Adrian Aug 24 '15 at 15:18
  • 1
    The `cornerRadius` approach is notorious for introducing undesirable artifacts when you look really closely at the corners. I'd suggest fixing the `UIBezierPath` approach (as A-Live suggested in comments above). – Rob Aug 24 '15 at 15:26
  • Put your resizing logic into the `layoutSubviews` override. You can then use the `cornerRadius` technique or masking, as you prefer. – Mundi Aug 24 '15 at 15:26
  • @Rob not with `width / 2.0`. Perfect circles. – Mundi Aug 24 '15 at 15:27
  • @Mundi: yes, your solution works, but I want to press a button and then the circular image view should get smaller, but after getting smaller it is not a perfect circle anymore. I am not sure if this aproach is bad or it's something with auto layout. – Adrian Aug 24 '15 at 15:44
  • Btw, sorry for the noob question but how do I have access to layoutSubviews ? what do I need to override ? – Adrian Aug 24 '15 at 15:55
  • @Mundi - Yes, sometimes even with `width / 2.0`, the `cornerRadius` approach can have problems. First a `UIBezierPath` with `bezierPathWithArcCenter` is sometimes smoother than corner radius approach (or `ovalInRect` approach). Second, you definitely can end up with weird little artifacts (see http://i.stack.imgur.com/DrRHu.png for an example) with `cornerRadius` approach. Don't get me wrong, sometimes `cornerRadius` works wonderfully, in which case you should use it. But in other cases, it really stinks and the `UIBezierPath` approach generates far more elegant results. – Rob Aug 24 '15 at 16:03
  • @Rob This is interesting. Perhaps, if you have time, you should post a question and answer it yourself (with screenshots) for the benefit or others. – Mundi Aug 25 '15 at 00:00
  • This actually has been asked before. See http://stackoverflow.com/q/31602251 or http://stackoverflow.com/q/28822940 or http://stackoverflow.com/q/30124896 for a few. – Rob Aug 25 '15 at 01:59