-1

I'm using this approach to cut out a rounded rect "window" from a background view:

override func draw(_ rect: CGRect) {
        guard let rectsArray = rectsArray else {
            return
        }

        for holeRect in rectsArray {
            let holeRectIntersection = rect.intersection(holeRect)

            if let context = UIGraphicsGetCurrentContext() {
                let roundedWindow = UIBezierPath(roundedRect: holeRect, cornerRadius: 15.0)
                if holeRectIntersection.intersects(rect) {
                    context.addPath(roundedWindow.cgPath)
                    context.clip()
                    context.clear(holeRectIntersection)
                    context.setFillColor(UIColor.clear.cgColor)
                    context.fill(holeRectIntersection)
                }
            }

        }
    }

In layoutSubviews() I update the background colour add my "window frame" rect:

override func layoutSubviews() {
        super.layoutSubviews()
        backgroundColor = self.baseMoodColour
        isOpaque = false
        self.rectsArray?.removeAll()
        self.rectsArray = [dragAreaView.frame]
}

I'm adding the rect here because layoutSubviews() updates the size of the "window frame" (i.e., the rect changes after layoutSubviews() runs).

The basic mechanism works as expected, however, if I change the background colour, the cutout window fills with black. So I'm wondering how I can animate a background colour change with this kind of setup? That is, I want to animate the colour of the area outside the cutout window (the window remains clear).

I've tried updating backgroundColor directly, and also using didSet in the accessor of a custom colour variable in my UIView subclass, but both cause the same filling-in of the "window".

    var baseMoodColour: UIColor {
        didSet {
            self.backgroundColor = baseMoodColour
            self.setNeedsDisplay()
        }
    }
jbm
  • 1,248
  • 10
  • 22
  • Sorry, I don't see any "change the background color" or "animate" code. Please show what you're talking about. – matt Jul 02 '19 at 16:33
  • Okay, I can update my question a little later, but I'm literally only talking about calling `self.backgroundColor = someNewColour`, somewhere else in the code. I have a colour variable that I call, and in the didSet I was assigning it to the view's backgroundColor. – jbm Jul 02 '19 at 16:45
  • But `self.backgroundColor = someNewColour` is not animation. What's the animation part? – matt Jul 02 '19 at 17:00
  • It doesn't matter. I can put the colour change in an animation block, but the colour still breaks the cutout. When I have a chance I'll rephrase the question to make the animation optional. Changing the background colour breaks the cutout "window". That's the main issue. – jbm Jul 02 '19 at 17:11
  • Not sure if downvotes will affect this solution being found by other users, but if so, this shouldn't be downvoted anymore. I understand why it was initially downvoted, but the question has been clarified and the solution has been provided. – jbm Jul 04 '19 at 23:16
  • If you think you have answered your own question, please enter the answer as an _answer_, not as part of the question – matt Jul 04 '19 at 23:39

3 Answers3

0

Try to use UIView.animate, you can check it here

UIView.animate(withDuration: 1.0, delay: 0.0, options: [.curveEaseOut], animations: {
  self.backgroundColor = someNewColour
  //Generally 
  //myView.backgroundColor = someNewColor
}, nil)
Andrew21111
  • 868
  • 8
  • 17
0

The problem in the short run is that that is simply what clear does if the background color is opaque. Just give your background color some transparency — even a tiny bit of transparency, so tiny that the human eye cannot perceive it — and now clear will cut a hole in the view.

For example, your code works fine if you set the view's background color to UIColor.green.withAlphaComponent(0.99).

By the way, you should delete the lines about UIColor.clear; that's a red herring. You should also cut the lines about the backgroundColor; you should not be repainting the background color into your context. They are two different things.


The problem in the long run is that what you're doing is not how to punch a hole in a view. You should be using a mask instead. That's the only way you're going to get the animation while maintaining the hole.

matt
  • 515,959
  • 87
  • 875
  • 1,141
  • Just to clarify; commenting out the first two lines of `draw(rect:)` indeed changes nothing, so those are gone. However, the `context.setFillColor(UIColor.clear.cgColor)` does appear to be essential in achieving the desired result (i.e., having another view show through the "window" cut out in this view). – jbm Jul 02 '19 at 21:23
  • I do need a "hole", as there is another view behind this one that needs to show through—this view is kind of like a picture frame or window frame around the other. But I'm curious about your statement: "That's the only way you're going to get the animation while maintaining the hole." What do you mean, exactly? Is it not possible to animate the colour with the approach I'm using (I kinda suspect not, which is why I posted here). If, indeed, it isn't possible, then how can it be done? My first attempt was actually using `mask`, but I couldn't get the cutout that way, when I tried. – jbm Jul 02 '19 at 21:28
  • "However, the context.setFillColor(UIColor.clear.cgColor) does appear to be essential in achieving the desired result (i.e., having another view show through the "window" cut out in this view)" Well I can prove that it isn't. – matt Jul 02 '19 at 22:49
  • 1
    "My first attempt was actually using mask, but I couldn't get the cutout that way, when I tried." Well go back and attempt it again, because that is exactly the kind of thing you can do with a mask. See for example my https://stackoverflow.com/a/23452614/341994 – matt Jul 03 '19 at 00:09
  • Excellent. Thanks for the link, I'll check it out a little later – jbm Jul 03 '19 at 00:24
0

Answering my own question, based on @matt's suggestion (and linked example), I did it with a CAShapeLayer. There was an extra "hitch" in my requirements, since I have a couple of views on top of the one I needed to mask out. So, I did the masking like this:

func cutOutWindow() {
        // maskedBackgroundView is an additional view, inserted ONLY for the mask
        let r = self.maskedBackgroundView.bounds
        // Adjust frame for dragAreaView's border
        var dragSize = self.dragAreaView.frame.size
        var dragPosition = self.dragAreaView.frame.origin
        dragSize.width -= 6.0
        dragSize.height -= 6.0
        dragPosition.x += 3.0
        dragPosition.y += 3.0
        let r2 = CGRect(x: dragPosition.x, y: dragPosition.y, width: dragSize.width, height: dragSize.height)
        let roundedWindow = UIBezierPath(roundedRect: r2, cornerRadius: 15.0)
        let mask = CAShapeLayer()
        let path = CGMutablePath()
        path.addPath(roundedWindow.cgPath)
        path.addRect(r)
        mask.path = path
        mask.fillRule = kCAFillRuleEvenOdd
        self.maskedBackgroundView.layer.mask = mask
    }

Then I had to apply the colour change to maskedBackgroundView.layer.backgroundColor (i.e., to the layer, not the view). With that in place, I get the cutout I need, with animatable colour changes. Thanks @matt for pointing me in the right direction.

jbm
  • 1,248
  • 10
  • 22