'SwiftUI add inverted mask
I'm trying to add a mask to two shapes such that the second shape masks out the first shape. If I do something like Circle().mask(Circle().offset(…))
, this has the opposite affect: preventing anything outside the first circle from being visible.
For UIView
the answer is here: iOS invert mask in drawRect
However, trying to implement this in SwiftUI without UIView
eludes me. I tried implementing an InvertedShape with I could then use as a mask:
struct InvertedShape<OriginalType: Shape>: Shape {
let originalShape: OriginalType
func path(in rect: CGRect) -> Path {
let mutableOriginal = originalShape.path(in: rect).cgPath.mutableCopy()!
mutableOriginal.addPath(Path(rect).cgPath)
return Path(mutableOriginal)
.evenOddFillRule()
}
}
Unfortunately, SwiftUI paths do not have addPath(Path)
(because they are immutable) or evenOddFillRule()
. You can access the path's CGPath and make a mutable copy and then add the two paths, however, evenOddFillRule
needs to be set on the CGLayer
, not the CGPath
. So unless I can get to the CGLayer, I'm unsure how to proceed.
This is Swift 5.
Solution 1:[1]
Here is a demo of possible approach of creating inverted mask, by SwiftUI only, (on example to make a hole in view)
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)))
}
}
Solution 2:[2]
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.
Solution 3:[3]
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
}
}
Solution 4:[4]
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()
}
Solution 5:[5]
I haven't tested this yet, but could you do something like this:
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
}
}
struct MaskView: UIViewRepresentable {
@Binding var child: UIHostingController<ImageView>
@Binding var rect: CGRect
@Binding var invert: Bool
func makeUIView(context: UIViewRepresentableContext<MaskView>) -> UIView {
let view = UIView()
self.child.view.mask(self.rect, invert: self.invert)
view.addSubview(self.child.view)
return view
}
func updateUIView(_ uiView: UIView, context: UIViewRepresentableContext<MaskView>) {
}
}
Usage:
struct ImageView: View {
var body: some View {
Image("image1")
}
}
struct ContentView: View {
@State var child = UIHostingController(rootView: ImageView())
@State var rect: CGRect = CGRect(x: 50, y: 50, width: 50, height: 50)
@State var invert: Bool = false
var body: some View {
VStack(alignment: .leading) {
MaskView(child: self.$child, rect: self.$rect, invert: self.$invert)
}
}
}
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | |
Solution 2 | neoneye |
Solution 3 | |
Solution 4 | |
Solution 5 |