You'll want to get the frame the image occupies within the image view.
Unfortunately, UIImageView
provides no native support for doing this, however you can calculate this fairly simply. I have already created a function that will take a given outer rect, and a given inner rect and return the inner rect after it's been aspect fitted to sit within the outer rect.
A Swift version of the function would look something like this:
func aspectFitRect(outerRect outerRect:CGRect, innerRect:CGRect) -> CGRect {
let innerRectRatio = innerRect.size.width/innerRect.size.height; // inner rect ratio
let outerRectRatio = outerRect.size.width/outerRect.size.height; // outer rect ratio
// calculate scaling ratio based on the width:height ratio of the rects.
let ratio = (innerRectRatio > outerRectRatio) ? outerRect.size.width/innerRect.size.width:outerRect.size.height/innerRect.size.height;
// The x-offset of the inner rect as it gets centered
let xOffset = (outerRect.size.width-(innerRect.size.width*ratio))*0.5;
// The y-offset of the inner rect as it gets centered
let yOffset = (outerRect.size.height-(innerRect.size.height*ratio))*0.5;
// aspect fitted origin and size
let innerRectOrigin = CGPoint(x: xOffset+outerRect.origin.x, y: yOffset+outerRect.origin.y);
let innerRectSize = CGSize(width: innerRect.size.width*ratio, height: innerRect.size.height*ratio);
return CGRect(origin: innerRectOrigin, size: innerRectSize);
}
The other thing you need to do is subclass UIImageView
and override the layoutSubviews
method. This is because as you're adding your image views to a UIStackView
- you're no longer in control of the frames of your image views. Therefore by overriding layoutSubviews
, you'll be able to update your mask whenever the stack view alters the frame of the view.
Something like this should achieve the desired result:
class MaskedImageView: UIImageView {
let maskLayer = CAShapeLayer()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
override init(image: UIImage?) {
super.init(image: image)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
private func commonInit() {
// configure your common image view properties here
contentMode = .ScaleAspectFit
clipsToBounds = true
// mask your image layer
layer.mask = maskLayer
}
override func layoutSubviews() {
guard let img = image else { // if there's no image - skip updating the mask.
return
}
// the frame that the image itself will occupy in the image view as it gets aspect fitted
let imageRect = aspectFitRect(outerRect: bounds, innerRect: CGRect(origin: CGPointZero, size: img.size))
// update mask frame
maskLayer.frame = imageRect
// half the image's on-screen width or height, whichever is smallest
let radius = min(imageRect.size.width, imageRect.size.height)*0.5
// the center of the image rect
let center = CGPoint(x: imageRect.size.width*0.5, y: imageRect.size.height*0.5)
// your custom masking path
let path = UIBezierPath()
path.moveToPoint(center)
path.addArcWithCenter(center, radius: radius, startAngle: 0, endAngle: CGFloat(M_PI*2.0*0.8), clockwise: true)
path.closePath()
// update mask layer path
maskLayer.path = path.CGPath
}
}
You can then create your image views from your view controller and add them to your stack view as normal.
let stackView = UIStackView()
override func viewDidLoad() {
super.viewDidLoad()
stackView.frame = view.bounds
stackView.distribution = .FillProportionally
stackView.spacing = 10
view.addSubview(stackView)
for _ in 0..<5 {
let imageView = MaskedImageView(image:UIImage(named:"foo.jpg"))
stackView.addArrangedSubview(imageView)
stackView.layoutIfNeeded()
}
}
Gives me the following result:

Unrelated Ramblings...
Just noticed in your code that you're doing this:
testPicture.clipsToBounds = true
testPicture.layer.masksToBounds = true
These both do the same thing.
A UIView
is no more than a wrapper for an underlying CALayer
. However for convenience, some CALayer
properties also have a UIView
equivalent. All the UIView
equivalent does is forward to message down to the CALayer
when it is set, and retrieve a value from the CALayer
when it is 'get'ed.
clipsToBounds
and masksToBounds
are one of these pairs (although annoyingly they don't share the same name).
Try doing the following:
view.layer.masksToBounds = true
print(view.clipsToBounds) // output: true
view.layer.masksToBounds = false
print(view.clipsToBounds) // output: false
view.clipsToBounds = true
print(view.layer.masksToBounds) // output: true
view.clipsToBounds = false
print(view.layer.masksToBounds) // output: false
Seeing as you're working with a UIView
, clipToBounds
is generally the preferred property to update.