skip to Main Content

Background

I’m trying to create a mask using two UIView with the below code.

Masking works, but not in the way that I was intending, images below.

I’m trying to get masking to work like Image A, but I’m getting the masking in Image B.

Question

How do I create a mask like Image A with two UIView?

Code

import UIKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        
        let redView = UIView(frame: CGRect(x: 100, y: 100, width: 200, height: 200))
        redView.backgroundColor = .red
        view.addSubview(redView)
        
        let maskView = UIView(frame: CGRect(x: 0, y: 0, width: 200, height: 200))
        maskView.backgroundColor = .blue
        maskView.layer.cornerRadius = 100
        redView.mask = maskView
        
    }

}

Images
enter image description here

2

Answers


  1. Masking is something provided by the lower level Core Animation layers that back UIViews. You need to provide a LAYER to serve as a mask, not a view.

    That layer can either be a CGImage, in which case the alpha channel of the image is what determines the masking, or you can use a CAShapeLayer.

    For a circular shape like in your case a shape layer is likely the better choice.

    Edit:

    Check out the code in this answer:

    https://stackoverflow.com/a/22019632/205185

    Here’s the (very old Objective-C) code from that answer:

    CAShapeLayer *shape = [CAShapeLayer layer];
    
    shape.frame = self.bounds;
    
    CGMutablePathRef pathRef = CGPathCreateMutable();
    CGPathAddRect(pathRef, NULL, self.bounds);
    CGPathAddEllipseInRect(pathRef, NULL, self.bounds);
    
    shape.fillRule = kCAFillRuleEvenOdd;
    shape.path = pathRef;
    
    self.layer.mask = shape;
    

    The Swift code would look something like this:

    let shape = CAShapeLayer()
    shape.frame = self.bounds
    var path = CGPath()
    path.addRect(self.bounds)
    path.addEllipse(in: self.bounds)
    
    redView.mask = shape
    

    (I spent about a minute on that code. It likely contains a couple of syntax errors.)

    Also note that if your app supports resizing, you’ll need to update your mask path if the owning view changes size (during device rotation, for example.)

    Login or Signup to reply.
  2. The key observation is that a mask will reveal the underlying view/layer based upon the alpha channel of the mask. As the documentation says:

    The view’s alpha channel determines how much of the view’s content and background shows through. Fully or partially opaque pixels allow the underlying content to show through but fully transparent pixels block that content.

    Thus, a mask yields your scenario “B”.

    You ask:

    How do I create a mask like Image A with two UIView?

    In short, this sort of inverted masking is not something you can just do with view’s and applying a corner radius of one of their layers. We would generally achieve that sort of inverse mask by masking the view’s layer with a CAShapeLayer, where you can draw whatever shape you want.

    let origin = CGPoint(x: 100, y: 100)
    let size = CGSize(width: 200, height: 300)
    let cornerRadius: CGFloat = 100
    
    let blueView = UIView(frame: CGRect(origin: origin, size: size))
    blueView.backgroundColor = .blue
    view.addSubview(blueView)
    
    let rect = CGRect(origin: .zero, size: size)
    let path = UIBezierPath(rect: rect)
    path.append(UIBezierPath(roundedRect: rect, cornerRadius: cornerRadius))
    
    let maskLayer = CAShapeLayer()
    maskLayer.fillRule = .evenOdd
    maskLayer.path = path.cgPath
    
    blueView.layer.mask = maskLayer
    

    But, frankly, a UIViewController probably shouldn’t be reaching into a UIView and adjusting its layer’s mask. I would define a CornersView:

    class CornersView: UIView {
        var cornerRadius: CGFloat? { didSet { setNeedsLayout() } }
        
        override func layoutSubviews() {
            super.layoutSubviews()
    
            let radius = cornerRadius ?? min(bounds.width, bounds.height) / 2
            let path = UIBezierPath(rect: bounds)
            path.append(UIBezierPath(roundedRect: bounds, cornerRadius: radius))
            
            let maskLayer = CAShapeLayer()
            maskLayer.fillRule = .evenOdd
            maskLayer.path = path.cgPath
            layer.mask = maskLayer
        }
    }
    

    By putting the rounding logic in layoutSubviews, that means that this rounding will be applied automatically regardless of the size. Then, you would use it like so:

    let blueView = CornersView(frame: CGRect(x: 100, y: 100, width: 200, height: 200))
    blueView.backgroundColor = .blue
    view.addSubview(blueView)
    

    If you use storyboards, you might even make CornersView an @IBDesignable and make cornerRadius an @IBInspectable.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search