Kex
Kex

Reputation: 8629

Masking a donut shape in a UIImageView

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
    }

}

enter image description here

Is it possible to create a donut shaped cutout using masks?

Upvotes: 0

Views: 318

Answers (1)

MadProgrammer
MadProgrammer

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...

Those are the droids you're looking for

The green color is from the backgroundView on which the UIImageView resides

Now, if you would prefer something more like...

I can't see anything out of this helmet

Then just get rid of the second append statement, path.append(UIBezierPath(ovalIn: CGRect(x: 75, y: 75, width: 50, height: 50)))

Upvotes: 2

Related Questions