Triangular UIView or UIImageView - ios

I have requirement like below image.
But, i'm confused about how to achieve this? Can i achieved it by using 3 UIImageViews or UIViews. If both then, which one is better? Finally, i have to combine three images & make one from that three images. I should be able to get touch of image too. I have no idea about this. Thanks.

Every UIView has a backing CALayer (accessible by aview.layer).
Every CALayer has a mask property, which is another CALayer. A mask allows to define a see-through shape for the layer, like a stencil.
So you need three UIImageViews, each of them has a different .layer.mask, each of these masks is a different CAShapeLayer whose .path are triangular CGPaths.
// Build a triangular path
UIBezierPath *path = [UIBezierPath new];
[path moveToPoint:(CGPoint){0, 0}];
[path addLineToPoint:(CGPoint){40, 40}];
[path addLineToPoint:(CGPoint){100, 0}];
[path addLineToPoint:(CGPoint){0, 0}];
// Create a CAShapeLayer with this triangular path
// Same size as the original imageView
CAShapeLayer *mask = [CAShapeLayer new];
mask.frame = imageView.bounds;
mask.path = path.CGPath;
// Mask the imageView's layer with this shape
imageView.layer.mask = mask;
Repeat three times.

You can use UIBezierPath and CAShapeLayer to achieve this
Step1: Copy following code
TrImageView.swift
import UIKit
protocol TriImageViewDelegate: class {
func didTapImage(image: UIImage)
}
class TriImageView:UIView {
//assumption: view width = 2 x view height
var images = [UIImage]()
var delegate:TriImageViewDelegate?
override func awakeFromNib() {
super.awakeFromNib()
//add imageviews
for i in 1...3 {
let imageView = UIImageView()
imageView.tag = i
imageView.userInteractionEnabled = true
self.addSubview(imageView)
}
//add gesture recognizer
self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(TriImageView.handleTap(_:))))
}
//override drawRect
override func drawRect(rect: CGRect) {
super.drawRect(rect)
let width = rect.size.width
let height = rect.size.height
let frame = CGRect(x: 0, y: 0, width: width, height: height)
let pointA = CGPoint(x: 0, y: 0)
let pointB = CGPoint(x: width * 0.79, y: 0)
let pointC = CGPoint(x: width, y: 0)
let pointD = CGPoint(x: width * 0.534,y: height * 0.29)
let pointE = CGPoint(x: 0, y: height * 0.88)
let pointF = CGPoint(x: 0, y: height)
let pointG = CGPoint(x: width * 0.874, y: height)
let pointH = CGPoint(x: width, y: height)
let path1 = [pointA,pointB,pointD,pointE]
let path2 = [pointE,pointD,pointG,pointF]
let path3 = [pointB,pointC,pointH,pointG,pointD]
let paths = [path1,path2,path3]
for i in 1...3 {
let imageView = (self.viewWithTag(i) as! UIImageView)
imageView.image = images[i - 1]
imageView.frame = frame
addMask(imageView, points: paths[i - 1])
}
}
//Add mask to the imageview
func addMask(view:UIView, points:[CGPoint]){
let maskPath = UIBezierPath()
maskPath.moveToPoint(points[0])
for i in 1..<points.count {
maskPath.addLineToPoint(points[i])
}
maskPath.closePath()
let maskLayer = CAShapeLayer()
maskLayer.path = maskPath.CGPath
view.layer.mask = maskLayer
}
//handle tap
func handleTap(recognizer:UITapGestureRecognizer){
let point = recognizer.locationInView(recognizer.view)
for i in 1...3 {
let imageView = (self.viewWithTag(i) as! UIImageView)
let layer = (imageView.layer.mask as! CAShapeLayer)
let path = layer.path
let contains = CGPathContainsPoint(path, nil, point, false)
if contains == true {
delegate?.didTapImage(imageView.image!)
}
}
}
}
Step2: Set the custom class
Step3: Use it

Related

Custom View Drawing - Hole inside a View

How to draw View like this.
After research I got context.fillRects method can be used. But how to find the exact rects for this.
let context = UIGraphicsGetCurrentContext()
context?.setFillColor(UIColor.red.cgColor)
context?.setAlpha(0.5)
context?.fill([<#T##rects: [CGRect]##[CGRect]#>])
How to achieve this result?
Background: Blue.
Overlay(Purple): 50% opacity that contains square hole in the center
First create your view and then draw everything with two UIBezierPaths: one is describing the inside rect (the hole) and the other one runs along the borders on your screen (externalPath). This way of drawing ensures that the blue rect in the middle is a true hole and not drawn on top of the purple view.
let holeWidth: CGFloat = 200
let hollowedView = UIView(frame: view.frame)
hollowedView.backgroundColor = UIColor.clear
//Initialise the layer
let hollowedLayer = CAShapeLayer()
//Draw your two paths and append one to the other
let holePath = UIBezierPath(rect: CGRect(origin: CGPoint(x: (view.frame.width - holeWidth) / 2, y: (view.frame.height - holeWidth) / 2), size: CGSize(width: holeWidth, height: holeWidth)))
let externalPath = UIBezierPath(rect: hollowedView.frame).reversing()
holePath.append(externalPath)
holePath.usesEvenOddFillRule = true
//Assign your path to the path property of your layer
hollowedLayer.path = holePath.cgPath
hollowedLayer.fillColor = UIColor.purple.cgColor
hollowedLayer.opacity = 0.5
//Add your hollowedLayer to the layer of your hollowedView
hollowedView.layer.addSublayer(hollowedLayer)
view.addSubview(hollowedView)
The result looks like this :
Create a custom UIView with background color blue.
class CustomView: UIView {
// Try adding a rect and fill color.
override func draw(_ rect: CGRect) {
let ctx = UIGraphicsGetCurrentContext()
ctx!.beginPath()
//Choose the size based on the size required.
ctx?.addRect(CGRect(x: 20, y: 20, width: rect.maxX - 40, height: rect.maxY - 40))
ctx!.closePath()
ctx?.setFillColor(UIColor.red.cgColor)
ctx!.fillPath()
}
}
I just ended up with this.
Code:
createHoleOnView()
let blurView = createBlurEffect(style: style)
self.addSubview(blurView)
Method Create Hole:
private func createHoleOnView() {
let maskView = UIView(frame: self.frame)
maskView.clipsToBounds = true;
maskView.backgroundColor = UIColor.clear
func holeRect() -> CGRect {
var holeRect = CGRect(x: 0, y: 0, width: scanViewSize.rawValue.width, height: scanViewSize.rawValue.height)
let midX = holeRect.midX
let midY = holeRect.midY
holeRect.origin.x = maskView.frame.midX - midX
holeRect.origin.y = maskView.frame.midY - midY
self.holeRect = holeRect
return holeRect
}
let outerbezierPath = UIBezierPath.init(roundedRect: self.bounds, cornerRadius: 0)
let holePath = UIBezierPath(roundedRect: holeRect(), cornerRadius: holeCornerRadius)
outerbezierPath.append(holePath)
outerbezierPath.usesEvenOddFillRule = true
let hollowedLayer = CAShapeLayer()
hollowedLayer.fillRule = kCAFillRuleEvenOdd
hollowedLayer.fillColor = outerColor.cgColor
hollowedLayer.path = outerbezierPath.cgPath
if self.holeStyle == .none {
hollowedLayer.opacity = 0.8
}
maskView.layer.addSublayer(hollowedLayer)
switch self.holeStyle {
case .none:
self.addSubview(maskView)
break
case .blur(_):
self.mask = maskView;
break
}
}
UIView's Extension function for Create Blur:
internal func createBlurEffect(style: UIBlurEffectStyle = .extraLight) -> UIView {
let blurEffect = UIBlurEffect(style: style)
let blurEffectView = UIVisualEffectView(effect: blurEffect)
blurEffectView.frame = self.bounds
return blurEffectView
}

Adding border to mask layer

I'm trying to make a custom shape UIButton using mask layers and I was successful
extension UIButton {
func mask(withImage image : UIImage , frame : CGRect ){
let maskingLayer = CAShapeLayer()
maskingLayer.frame = frame
maskingLayer.contents = image.cgImage
self.layer.mask = maskingLayer
}
}
But I want to add a border (stroke) to the mask layer to indicate that this button was chosen.
I tried adding a sub layer to the mask layer
button.mask?.layer.borderColor = UIColor.black.cgColor
button.mask?.layer.borderWidth = 4
but didn't work since the button shape is still rectangle.
I know I can use CGMutablePath to define the shape
func mask(withPath path: CGMutablePath , frame : CGRect , color : UIColor) {
let mask = CAShapeLayer()
mask.frame = frame
mask.path = path
self.layer.mask = mask
let shape = CAShapeLayer()
shape.frame = self.bounds
shape.path = path
shape.lineWidth = 3.0
shape.strokeColor = UIColor.black.cgColor
shape.fillColor = color.cgColor
self.layer.insertSublayer(shape, at: 0)
// self.layer.sublayers![0].masksToBounds = true
}
but drawing such complex shape using paths is extremly hard
Any help would be appreciated.
i was able to get CGPath from an image(.svg) using PocketSVG
but i faced another problem that the scale of the path is equal to the original SVG so i managed to scale the path to fit in frame , here is the full code :-
extension UIView {
func mask(withSvgName ImageName : String , frame : CGRect , color : UIColor){
let svgutils = SvgUtils()
let paths = svgutils.getLayerFromSVG(withImageName: ImageName)
let mask = CAShapeLayer()
mask.frame = frame
let newPath = svgutils.resizepath(Fitin: frame, path: paths[0].cgPath)
mask.path = newPath
self.layer.mask = mask
let shape = CAShapeLayer()
shape.frame = self.bounds
shape.path = newPath
shape.lineWidth = 2.0
shape.strokeColor = UIColor.black.cgColor
shape.fillColor = color.cgColor
self.layer.insertSublayer(shape, at: 0)
}
}
and the utilities class
import Foundation
import PocketSVG
class SvgUtils{
func getLayerFromSVG(withImageName ImageName : String ) -> [SVGBezierPath]{
let url = Bundle.main.url(forResource: ImageName, withExtension: "svg")!
var paths = [SVGBezierPath]()
for path in SVGBezierPath.pathsFromSVG(at: url) {
paths.append(path)
}
return paths
}
func resizepath(Fitin frame : CGRect , path : CGPath) -> CGPath{
let boundingBox = path.boundingBox
let boundingBoxAspectRatio = boundingBox.width / boundingBox.height
let viewAspectRatio = frame.width / frame.height
var scaleFactor : CGFloat = 1.0
if (boundingBoxAspectRatio > viewAspectRatio) {
// Width is limiting factor
scaleFactor = frame.width / boundingBox.width
} else {
// Height is limiting factor
scaleFactor = frame.height / boundingBox.height
}
var scaleTransform = CGAffineTransform.identity
scaleTransform = scaleTransform.scaledBy(x: scaleFactor, y: scaleFactor)
scaleTransform.translatedBy(x: -boundingBox.minX, y: -boundingBox.minY)
let scaledSize = boundingBox.size.applying(CGAffineTransform (scaleX: scaleFactor, y: scaleFactor))
let centerOffset = CGSize(width: (frame.width - scaledSize.width ) / scaleFactor * 2.0, height: (frame.height - scaledSize.height) / scaleFactor * 2.0 )
scaleTransform = scaleTransform.translatedBy(x: centerOffset.width, y: centerOffset.height)
//CGPathCreateCopyByTransformingPath(path, &scaleTransform)
let scaledPath = path.copy(using: &scaleTransform)
return scaledPath!
}
}
and simply use it like this
button.mask(withSvgName: "your_svg_fileName", frame: button.bounds, color: UIColor.green)

Thin border when using CAShapeLayer as mask for CAShapeLayer

In Swift, I have two semi-transparent circles, both of which are CAShapeLayer. Since they are semi-transparent, any overlap between them becomes visible like so:
Instead, I want them to visually "merge" together. The solution I have tried is to use circle 2 as a mask for circle 1, therefore cutting away the overlap.
This solution is generally working, but I get a thin line on the outside of circle 2:
My question: How can I get rid of the thin, outside line on the right circle? Why is it even there?
The code is as follows (Xcode playground can be found here):
private let yPosition: CGFloat = 200
private let circle1Position: CGFloat = 30
private let circle2Position: CGFloat = 150
private let circleDiameter: CGFloat = 200
private var circleRadius: CGFloat { return self.circleDiameter/2.0 }
override func loadView() {
let view = UIView()
view.backgroundColor = .black
self.view = view
let circle1Path = UIBezierPath(
roundedRect: CGRect(
x: circle1Position,
y: yPosition,
width: circleDiameter,
height: circleDiameter),
cornerRadius: self.circleDiameter)
let circle2Path = UIBezierPath(
roundedRect: CGRect(
x: circle2Position,
y: yPosition,
width: circleDiameter,
height: circleDiameter),
cornerRadius: self.circleDiameter)
let circle1Layer = CAShapeLayer()
circle1Layer.path = circle1Path.cgPath
circle1Layer.fillColor = UIColor.white.withAlphaComponent(0.6).cgColor
let circle2Layer = CAShapeLayer()
circle2Layer.path = circle2Path.cgPath
circle2Layer.fillColor = UIColor.white.withAlphaComponent(0.6).cgColor
self.view.layer.addSublayer(circle1Layer)
self.view.layer.addSublayer(circle2Layer)
//Create a mask from the surrounding rectangle of circle1, and
//then cut out where it overlaps circle2
let maskPath = UIBezierPath(rect: CGRect(x: circle1Position, y: yPosition, width: circleDiameter, height: circleDiameter))
maskPath.append(circle2Path)
maskPath.usesEvenOddFillRule = true
maskPath.lineWidth = 0
let maskLayer = CAShapeLayer()
maskLayer.path = maskPath.cgPath
maskLayer.fillColor = UIColor.black.cgColor
maskLayer.fillRule = kCAFillRuleEvenOdd
circle1Layer.mask = maskLayer
}
If both CAShapeLayers have the same alpha value, you could place them inside a new parent CALayer then set the alpha of the parent instead.

How to set UIImageView with rounded corners for aspect fit mode

I usually use the following code to set rounded corners.
imageView.layer.cornerRadius = 10
It works when the imageView is set at Aspect Fill.
But when the imageView is set to Aspect Fit mode, and the ratio between imageView and picture are different.
The rounded corners effect won't be able to tell.
The background color is set to green for showing the rounded corners.
Is there any way to set 'real image part' to rounded corners.
Thank you in advance for your answers.
Use this extension to UIImageView:
extension UIImageView
{
func roundCornersForAspectFit(radius: CGFloat)
{
if let image = self.image {
//calculate drawingRect
let boundsScale = self.bounds.size.width / self.bounds.size.height
let imageScale = image.size.width / image.size.height
var drawingRect: CGRect = self.bounds
if boundsScale > imageScale {
drawingRect.size.width = drawingRect.size.height * imageScale
drawingRect.origin.x = (self.bounds.size.width - drawingRect.size.width) / 2
} else {
drawingRect.size.height = drawingRect.size.width / imageScale
drawingRect.origin.y = (self.bounds.size.height - drawingRect.size.height) / 2
}
let path = UIBezierPath(roundedRect: drawingRect, cornerRadius: radius)
let mask = CAShapeLayer()
mask.path = path.cgPath
self.layer.mask = mask
}
}
}
Swift 3 version of the helpful, accepted answer is over here!
extension UIImageView {
func roundCornersForAspectFit(radius: CGFloat)
{
if let image = self.image {
//calculate drawingRect
let boundsScale = self.bounds.size.width / self.bounds.size.height
let imageScale = image.size.width / image.size.height
var drawingRect : CGRect = self.bounds
if boundsScale > imageScale {
drawingRect.size.width = drawingRect.size.height * imageScale
drawingRect.origin.x = (self.bounds.size.width - drawingRect.size.width) / 2
}else {
drawingRect.size.height = drawingRect.size.width / imageScale
drawingRect.origin.y = (self.bounds.size.height - drawingRect.size.height) / 2
}
let path = UIBezierPath(roundedRect: drawingRect, cornerRadius: radius)
let mask = CAShapeLayer()
mask.path = path.cgPath
self.layer.mask = mask
}
}
}
Try this may help you:
import UIKit
class ViewController: UIViewController{
#IBOutlet weak var myImageView: UIImageView!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
myImageView.contentMode = UIViewContentMode.ScaleAspectFit
myImageView.clipsToBounds = true
//myImageView.layer.cornerRadius = 10.0
myImageView.layer.masksToBounds = true
let simpleImage = UIImage(named:"ipad5_einladung.jpg")
let corneredImage = generateRoundCornerImage(simpleImage!, radius: 10)
//Set cornered Image
myImageView.image = corneredImage;
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func generateRoundCornerImage(image : UIImage , radius : CGFloat) -> UIImage {
let imageLayer = CALayer()
imageLayer.frame = CGRectMake(0, 0, image.size.width, image.size.height)
imageLayer.contents = image.CGImage
imageLayer.masksToBounds = true
imageLayer.cornerRadius = radius
UIGraphicsBeginImageContext(image.size)
imageLayer.renderInContext(UIGraphicsGetCurrentContext())
let roundedImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return roundedImage
}
}
You first need to set the width and height to the same value. Then set the image properties like so:
imgProfile_Pic.layer.cornerRadius = cell.imgProfile_Pic.frame.size.height / 2
imgProfile_Pic.layer.borderWidth = 3.0
imgProfile_Pic.layer.borderColor = UIColor.white.cgColor
imgProfile_Pic.clipsToBounds = true
imgProfile_Pic.layoutIfNeeded()
This is the accepted answer converted to Objective-C:
#implementation UIImageView(Utils)
- (void)roundCornersForAspectFitWithRadius:(CGFloat)cornerRadius {
if (self.image) {
double boundsScale = self.bounds.size.width / self.bounds.size.height;
double imageScale = self.image.size.width / self.image.size.height;
CGRect drawingRect = self.bounds;
if (boundsScale > imageScale) {
drawingRect.size.width = drawingRect.size.height * imageScale;
drawingRect.origin.x = (self.bounds.size.width - drawingRect.size.width) / 2;
}
else {
drawingRect.size.height = drawingRect.size.width / imageScale;
drawingRect.origin.y = (self.bounds.size.height - drawingRect.size.height) / 2;
}
UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:drawingRect cornerRadius:cornerRadius];
CAShapeLayer *mask = [CAShapeLayer new];
[mask setPath:path.CGPath];
[self.layer setMask:mask];
}
}
#end
I wanted to comment on Arun's answer to help those who are having issues with doing Arun's method but in Collection View, you have to initiate the imageView's frame as the collection view cell's frame.
i.e.
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "identifier", for: indexPath) as UICollectionViewCell
let imageView = UIImageView(frame: cell.frame)
obviously you want to refactor that and stuff but you get the idea.
If you use SDWebImage in your project, you can do the following:
myUIImageView.image = myUIImage.sd_roundedCornerImage(withRadius: 24, corners: .allCorners, borderWidth: 0, borderColor: nil)
This is very simple to round the image view like this:
self.profileImageView?.clipsToBounds = true
self.profileImageView!.layer.cornerRadius = 10
self.profileImageView?.layer.borderWidth = 1.0
self.profileImageView?.contentMode = .ScaleAspectFit
But for making image view rounded with image ratio, I think you have to set image view for ScaleAspectFill mode.

iOS invert mask in drawRect

With the code below, I am successfully masking part of my drawing, but it's the inverse of what I want masked. This masks the inner portion of the drawing, where I would like to mask the outer portion. Is there a simple way to invert this mask?
myPath below is a UIBezierPath.
CAShapeLayer *maskLayer = [[CAShapeLayer alloc] init];
CGMutablePathRef maskPath = CGPathCreateMutable();
CGPathAddPath(maskPath, nil, myPath.CGPath);
[maskLayer setPath:maskPath];
CGPathRelease(maskPath);
self.layer.mask = maskLayer;
With even odd filling on the shape layer (maskLayer.fillRule = kCAFillRuleEvenOdd;) you can add a big rectangle that covers the entire frame and then add the shape you are masking out. This will in effect invert the mask.
CAShapeLayer *maskLayer = [[CAShapeLayer alloc] init];
CGMutablePathRef maskPath = CGPathCreateMutable();
CGPathAddRect(maskPath, NULL, someBigRectangle); // this line is new
CGPathAddPath(maskPath, nil, myPath.CGPath);
[maskLayer setPath:maskPath];
maskLayer.fillRule = kCAFillRuleEvenOdd; // this line is new
CGPathRelease(maskPath);
self.layer.mask = maskLayer;
For Swift 3.0
func mask(viewToMask: UIView, maskRect: CGRect, invert: Bool = false) {
let maskLayer = CAShapeLayer()
let path = CGMutablePath()
if (invert) {
path.addRect(viewToMask.bounds)
}
path.addRect(maskRect)
maskLayer.path = path
if (invert) {
maskLayer.fillRule = kCAFillRuleEvenOdd
}
// Set the mask of the view.
viewToMask.layer.mask = maskLayer;
}
Based on the accepted answer, here's another mashup in Swift. I've made it into a function and made the invert optional
class func mask(viewToMask: UIView, maskRect: CGRect, invert: Bool = false) {
let maskLayer = CAShapeLayer()
let path = CGPathCreateMutable()
if (invert) {
CGPathAddRect(path, nil, viewToMask.bounds)
}
CGPathAddRect(path, nil, maskRect)
maskLayer.path = path
if (invert) {
maskLayer.fillRule = kCAFillRuleEvenOdd
}
// Set the mask of the view.
viewToMask.layer.mask = maskLayer;
}
Here's my Swift 4.2 solution that allows a corner radius
extension UIView {
func mask(withRect maskRect: CGRect, cornerRadius: CGFloat, inverse: Bool = false) {
let maskLayer = CAShapeLayer()
let path = CGMutablePath()
if (inverse) {
path.addPath(UIBezierPath(roundedRect: self.bounds, cornerRadius: cornerRadius).cgPath)
}
path.addPath(UIBezierPath(roundedRect: maskRect, cornerRadius: cornerRadius).cgPath)
maskLayer.path = path
if (inverse) {
maskLayer.fillRule = CAShapeLayerFillRule.evenOdd
}
self.layer.mask = maskLayer;
}
}
Swift 5
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let red = UIView(frame: view.bounds)
view.addSubview(red)
view.backgroundColor = UIColor.cyan
red.backgroundColor = UIColor.red
red.mask(CGRect(x: 50, y: 50, width: 50, height: 50), invert: true)
}
}
extension UIView{
func mask(_ rect: CGRect, invert: Bool = false) {
let maskLayer = CAShapeLayer()
let path = CGMutablePath()
if (invert) {
path.addRect(bounds)
}
path.addRect(rect)
maskLayer.path = path
if (invert) {
maskLayer.fillRule = CAShapeLayerFillRule.evenOdd
}
// Set the mask of the view.
layer.mask = maskLayer
}
}
Thanks #arvidurs
For Swift 4.2
func mask(viewToMask: UIView, maskRect: CGRect, invert: Bool = false) {
let maskLayer = CAShapeLayer()
let path = CGMutablePath()
if (invert) {
path.addRect(viewToMask.bounds)
}
path.addRect(maskRect)
maskLayer.path = path
if (invert) {
maskLayer.fillRule = .evenOdd
}
// Set the mask of the view.
viewToMask.layer.mask = maskLayer;
}
In order to invert mask you can to something like this
Here I have mask of crossed rectancles like
let crossPath = UIBezierPath(rect: cutout.insetBy(dx: 30, dy: -5))
crossPath.append(UIBezierPath(rect: cutout.insetBy(dx: -5, dy: 30)))
let crossMask = CAShapeLayer()
crossMask.path = crossPath.cgPath
crossMask.backgroundColor = UIColor.clear.cgColor
crossMask.fillRule = .evenOdd
And here I add 3rd rectancle around my crossed rectancles
so using .evenOdd it takes area that is equal to (New Rectancle - Old Crossed Rectangle) so in other words outside area of crossed rectancles
let crossPath = UIBezierPath(rect: cutout.insetBy(dx: -5, dy: -5) )
crossPath.append(UIBezierPath(rect: cutout.insetBy(dx: 30, dy: -5)))
crossPath.append(UIBezierPath(rect: cutout.insetBy(dx: -5, dy: 30)))
let crossMask = CAShapeLayer()
crossMask.path = crossPath.cgPath
crossMask.backgroundColor = UIColor.clear.cgColor
crossMask.fillRule = .evenOdd
There are many great answers, all of them using even-odd rule for resolving interior and exterior surfaces. Here is Swift 5 approach with non-zero rule:
extension UIView {
func mask(_ rect: CGRect, cornerRadius: CGFloat, invert: Bool = false) {
let maskLayer = CAShapeLayer()
let path = CGMutablePath()
if (invert) {
path.move(to: .zero)
path.addLine(to: CGPoint(x: 0, y: bounds.height))
path.addLine(to: CGPoint(x: bounds.width, y: bounds.height))
path.addLine(to: CGPoint(x: bounds.width, y: 0))
path.addLine(to: .zero)
}
path.addPath(UIBezierPath(roundedRect: rect, cornerRadius: cornerRadius).cgPath)
maskLayer.path = path
layer.mask = maskLayer
}
}
For masking operation it is pretty straight forward. We draw rect and use it as masking path.
For inverted masking we use bounds of the view that is going to be masked. Then we draw it counter clockwise. After that we add UIBezierPath rect, which is by default drawn clockwise. That way any point p inside the rect will have one clockwise intersection and one counter clockwise intersection, leading to total winding number of zero, making that point an exterior point.
For 2023. All answers currently here seem wrong as they don't adjust the mask when the layout changes (or - just one example - when the view is animating in size or shape).
It's very simple ..
1. have a layer (perhaps just a square of color, an image, whatever)
lazy var examp: CALayer = {
let l = CALayer()
l.background = UIColor.blue.cgColor
l.mask = shapeAsMask
layer.addSublayer(l)
return l
}()
Note that examp has its mask set already.
2. Whenever you want to mask, you of course need a masking layer on hand
lazy var shapeAsMask: CAShapeLayer = {
let s = CAShapeLayer()
s.fillRule = .evenOdd
layer.addSublayer(s)
layer.mask = s
return s
}()
Note that the fillrule is set as needed.
3. Now make a shape with a bezier curve. Let's just make a circle:
override func layoutSubviews() {
super.layoutSubviews()
examp.frame = bounds
let i = bounds.width * 0.30
let thing = UIBezierPath(ovalIn: bounds.insetBy(dx: i, dy: i))
...
}
So that's a small circle in the middle of the view.
If you want to "have the shape" it's just
override func layoutSubviews() {
super.layoutSubviews()
fill.frame = bounds
let i = bounds.width * 0.30
let thing = UIBezierPath(ovalIn: bounds.insetBy(dx: i, dy: i))
shapeAsMask.path = thing.cgPath
}
If you want to "have the INVERSE OF the shape" it's just
override func layoutSubviews() {
super.layoutSubviews()
fill.frame = bounds
let i = bounds.width * 0.30
let thing = UIBezierPath(ovalIn: bounds.insetBy(dx: i, dy: i))
let p = CGMutablePath()
p.addRect(bounds)
p.addPath(thing.cgPath)
shapeAsMask.path = p
}
In short, the "negative" of this ..
let thing = UIBezierPath( ... some path
is just this:
let neg = CGMutablePath()
neg.addRect(bounds)
neg.addPath(thing.cgPath)
So that's it.
Don't forget that ...
Anytime you have a mask (or a layer!) you have to set it in layoutSubviews. (That's why layoutSubviews is named layoutSubviews !)

Resources