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
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)
}
}
}
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
}
}
If you are after something like this:
Then you can just put the two shapes in a ZStack
and give the masking one the color of a background:
struct MaskView: View {
@Environment(\.colorScheme) var colorScheme: ColorScheme
var body: some View {
ZStack {
Circle()
.fill(Color.yellow)
Circle()
.fill(colorScheme == .dark ? Color.black : Color.white)
.offset(x: 150.0, y: 10.0)
}
}
}
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)))
}
}
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.