Reputation: 8629
I am trying to cut a donut shape into a UIImageView. I can currently only achieve a circle cutout like so:
import UIKit
import CoreGraphics
class AvatarImageView: UIImageView {
enum AvatarImageViewOnlineStatus {
case online
case offline
case hidden
}
var rounded: Bool = false
var onlineStatus: AvatarImageViewOnlineStatus = .hidden
override func layoutSubviews() {
super.layoutSubviews()
guard !rounded else { return }
if let image = image, !rounded {
self.image = image.af_imageRoundedIntoCircle()
rounded = true
}
let rect: CGRect = CGRect(x: 0, y: 0, width: frame.size.width, height: frame.size.height)
let maskLayer: CAShapeLayer = CAShapeLayer()
maskLayer.frame = rect
let mainPath: UIBezierPath = UIBezierPath(rect: rect)
let circlePath: UIBezierPath = UIBezierPath(ovalIn: CGRect(x: 20, y: 20, width: 30, height: 30))
mainPath.append(circlePath)
maskLayer.fillRule = kCAFillRuleEvenOdd
maskLayer.path = mainPath.cgPath
maskLayer.strokeColor = UIColor.clear.cgColor
maskLayer.lineWidth = 10.0
layer.mask = maskLayer
}
}
Is it possible to create a donut shaped cutout using masks?
Upvotes: 0
Views: 318
Reputation: 347314
This took a little more effort then I was expecting and the solution wasn't obvious ... until I found it, then it was like, oh, yeah, sure :/
The "basic" idea is, you want to "cut out" a section of the an existing path, the solution wasn't obvious, because UIBezierPath
doesn't provide a "subtract" or "remove" method. Instead, you need to "reverse" the path, for example...
let path = UIBezierPath(rect: CGRect(x: 0, y: 0, width: 200, height: 200))
path.append(UIBezierPath(ovalIn: CGRect(x: 50, y: 50, width: 100, height: 100)).reversing())
path.append(UIBezierPath(ovalIn: CGRect(x: 75, y: 75, width: 50, height: 50)))
And, because "out-of-context" snippets are rarely helpful, this is the playground I used to test it (supply own image)...
import UIKit
import Foundation
import CoreGraphics
let image = UIImage(named: "Profile")
let imageView = UIImageView(frame: CGRect(x: 0, y: 0, width: 200, height: 200))
let backgroundView = UIView(frame: CGRect(x: 0, y: 0, width: 200, height: 200))
backgroundView.backgroundColor = UIColor.green
backgroundView.addSubview(imageView)
imageView.contentMode = .scaleAspectFit
imageView.image = image
imageView.layer.cornerRadius = 100
let shapeLayer = CAShapeLayer()
let path = UIBezierPath(rect: CGRect(x: 0, y: 0, width: 200, height: 200))
path.append(UIBezierPath(ovalIn: CGRect(x: 50, y: 50, width: 100, height: 100)).reversing())
path.append(UIBezierPath(ovalIn: CGRect(x: 75, y: 75, width: 50, height: 50)))
shapeLayer.path = path.cgPath
shapeLayer.fillColor = UIColor.red.cgColor
shapeLayer.frame = imageView.bounds
imageView.layer.mask = shapeLayer
backgroundView
And my results...
The green color is from the backgroundView
on which the UIImageView
resides
Now, if you would prefer something more like...
Then just get rid of the second append
statement, path.append(UIBezierPath(ovalIn: CGRect(x: 75, y: 75, width: 50, height: 50)))
Upvotes: 2