Search code examples
iosswiftuiimageviewuibezierpathcgpath

Cut transparent hole in UIView using UIImageView


I'm trying to create the image below for my QR camera view.

enter image description here

I have an image for the camera overlay (which is the square in the middle of the screen).

Ive looked at similar topics on stackoverflow and found out how to cut out the camera overlay image from the black background (0.75% transparent) so that it leaves the empty space in the middle, however I'm having some real issues with its placement, and I cant find out what is behaving so weirdly.

Here is the code that I use to create the background black image and also to cut out the square in the center:

            // Create a view filling the screen.
        let backgroundView = UIView(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height))

        // Set a semi-transparent, black background.
        backgroundView.backgroundColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.75)

        // Create the initial layer from the view bounds.
        let maskLayer = CAShapeLayer()
        maskLayer.frame = backgroundView.bounds
        maskLayer.fillColor = UIColor.black.cgColor

        // Create the path.
        let path = UIBezierPath(rect: backgroundView.bounds)
        maskLayer.fillRule = CAShapeLayerFillRule.evenOdd

        // Append the overlay image to the path so that it is subtracted.
        path.append(UIBezierPath(rect: camOverlayImageView.frame))
        maskLayer.path = path.cgPath

        // Set the mask of the view.
        backgroundView.layer.mask = maskLayer

        // Add the view so it is visible.
        self.view.addSubview(backgroundView)

The camOverlayImageView is created in storyboard and all I'm using on it are constraints to center it both vertically and horizontally to the superview like this:

enter image description here

However, when I do this, this is what I'm getting on the device:

enter image description here

If anyone might know what might be causing this or how to fix it, it would be greatly appreciated as I can't seem to find it.

I can of course manually move the frame and offset it like this but that isn't the correct way to do this:

            let overlayFrame = camOverlayImageView.frame
        // Append the overlay image to the path so that it is subtracted.
        path.append(UIBezierPath(rect: CGRect(
            x: overlayFrame.origin.x + 18,
            y: overlayFrame.origin.y - 6,
            width: overlayFrame.size.width,
            height: overlayFrame.size.height))
        )
        maskLayer.path = path.cgPath

Instead of doing what I was previously doing:

path.append(UIBezierPath(rect: camOverlayImageView.frame))
maskLayer.path = path.cgPath

Solution

  • Most likely...

    You are creating your mask too early - before auto-layout has sized / positioned the views.

    Try it like this:

    class HoleInViewController: UIViewController {
    
        @IBOutlet var camOverlayImageView: UIView!
    
        let backgroundView: UIView = {
            let v = UIView()
            v.translatesAutoresizingMaskIntoConstraints = false
            v.backgroundColor = UIColor.black.withAlphaComponent(0.75)
            return v
        }()
    
        override func viewDidLoad() {
            super.viewDidLoad()
    
            view.addSubview(backgroundView)
            NSLayoutConstraint.activate([
                backgroundView.topAnchor.constraint(equalTo: view.topAnchor),
                backgroundView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
                backgroundView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
                backgroundView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
            ])
    
        }
    
        override func viewDidLayoutSubviews() {
            super.viewDidLayoutSubviews()
    
            // Create the initial layer from the view bounds.
            let maskLayer = CAShapeLayer()
            maskLayer.frame = backgroundView.bounds
            maskLayer.fillColor = UIColor.black.cgColor
    
            // Create the path.
            let path = UIBezierPath(rect: backgroundView.bounds)
            maskLayer.fillRule = CAShapeLayerFillRule.evenOdd
    
            // Append the overlay image to the path so that it is subtracted.
            path.append(UIBezierPath(rect: camOverlayImageView.frame))
            maskLayer.path = path.cgPath
    
            // Set the mask of the view.
            backgroundView.layer.mask = maskLayer
    
        }
    
    }