SwiftUI add inverted mask

前端 未结 5 1210
醉话见心
醉话见心 2020-12-15 22:53

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

相关标签:
5条回答
  • 2020-12-15 23:02

    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)
            }
        }
    }
    
    0 讨论(0)
  • 2020-12-15 23:03

    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
        }
    }
    
    0 讨论(0)
  • 2020-12-15 23:06

    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)
            }
        }
    }
    
    0 讨论(0)
  • 2020-12-15 23:15

    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)))
        }
    }
    
    0 讨论(0)
  • 2020-12-15 23:16

    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.

    0 讨论(0)
提交回复
热议问题