SwiftUI add inverted mask

Here is a demo of possible approach of creating inverted mask, by SwiftUI only, (on example to make a hole in view)

SwiftUI hole mask, reverse mask

func HoleShapeMask(in rect: CGRect) -> Path {
    var shape = Rectangle().path(in: rect)
    shape.addPath(Circle().path(in: rect))
    return shape
}

struct TestInvertedMask: View {

    let rect = CGRect(x: 0, y: 0, width: 300, height: 100)
    var body: some View {
        Rectangle()
            .fill(Color.blue)
            .frame(width: rect.width, height: rect.height)
            .mask(HoleShapeMask(in: rect).fill(style: FillStyle(eoFill: true)))
    }
}

Based on this article, here's a .reverseMask modifier you can use instead of .mask. I modified it to support iOS 13 and up.

extension View {
    @inlinable func reverseMask<Mask: View>(
        alignment: Alignment = .center,
        @ViewBuilder _ mask: () -> Mask
    ) -> some View {
            self.mask(
                ZStack {
                    Rectangle()

                    mask()
                        .blendMode(.destinationOut)
                }
            )
        }
}

Usage:

ViewToMask()
.reverseMask {
    MaskView()
}

Using a mask such as in the accepted answer is a good approach. Unfortunately, masks do not affect hit testing. Making a shape with a hole can be done in the following way.

extension Path {
    var reversed: Path {
        let reversedCGPath = UIBezierPath(cgPath: cgPath)
            .reversing()
            .cgPath
        return Path(reversedCGPath)
    }
}

struct ShapeWithHole: Shape {
    func path(in rect: CGRect) -> Path {
        var path = Rectangle().path(in: rect)
        let hole = Circle().path(in: rect).reversed
        path.addPath(hole)
        return path
    }
}

The trick is to reverse the path for the hole. Unfortunately Path does not (yet) support reversing the path out-of-the-box, hence the extension (which uses UIBezierPath). The shape can then be used for clipping and hit-testing purposes:

struct MaskedView: View {

    var body: some View {
        Rectangle()
            .fill(Color.blue)
            .frame(width: 300, height: 100)
            .clipShape(ShapeWithHole())    // clips or masks the view
            .contentShape(ShapeWithHole()) // needed for hit-testing
    }
}

Here's another way to do it, which is more Swiftly.

The trick is to use:

YourMaskView()
   .compositingGroup()
   .luminanceToAlpha() 

maskedView.mask(YourMaskView())

Just create your mask with Black and White shapes, black will be transparent, white opaque, anything in between is going to be semi-transparent.

.compositingView(), similar to .drawingGroup(), rasterises the view (converts it to a bitmap texture). By the way, this also happens when you .blur or do any other pixel-level operations.

.luminanceToAlpha() takes the RGB luminance levels (I guess by averaging the RGB values), and maps them to the Alpha (opacity) channel of the bitmap.

Tags:

Swift

Swiftui