SwiftUI добавляет инвертированную маску

Я пытаюсь добавить маску к двум фигурам так, чтобы вторая фигура маскировала первую фигуру. Если я сделаю что-то вродеCircle().mask(Circle().offset(…)), это имеет противоположный эффект: предотвращает отображение всего, что находится за пределами первого круга.

За UIViewответ здесь: инвертировать маску iOS в drawRect

Однако, пытаясь реализовать это в SwiftUI без UIViewускользает от меня. Я попытался реализовать InvertedShape, который затем можно было использовать в качестве маски:

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()
    }
}

К сожалению, пути SwiftUI не имеют addPath(Path) (потому что они неизменны) или evenOddFillRule(). Вы можете получить доступ к CGPath пути и сделать изменяемую копию, а затем добавить два пути, однако,evenOddFillRule необходимо установить на CGLayer, не CGPath. Так что, если я не смогу добраться до CGLayer, я не знаю, как действовать дальше.

Это Swift 5.

8 ответов

Решение

Вот демонстрация возможного подхода к созданию перевернутой маски только с помощью SwiftUI (на примере, чтобы сделать отверстие в поле зрения)

Маска с отверстиями SwiftUI, обратная маска

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)))
    }
}

Вот еще один способ сделать это быстрее.

Уловка заключается в том, чтобы использовать:

YourMaskView()
   .compositingGroup()
   .luminanceToAlpha() 

maskedView.mask(YourMaskView())

Просто создайте маску с помощью черно-белых форм, черный будет прозрачным, белый непрозрачным, все, что находится между ними, будет полупрозрачным.

.compositingView(), похожий на .drawingGroup(), растрирует вид (преобразует его в растровую текстуру). Кстати, такое бывает и при.blur или выполните любые другие операции на уровне пикселей.

.luminanceToAlpha() принимает уровни яркости RGB (я предполагаю, усредняя значения RGB) и сопоставляет их с каналом Alpha (непрозрачность) растрового изображения.

Используйте модификатор .blendMode

      ZStack {
      Rectangle() // destination
      Circle()    // source
        .blendMode(.destinationOut)
    }
    .compositingGroup()

Использование маски, такой как в принятом ответе, - хороший подход. К сожалению, маски не влияют на проверку попадания. Сделать фигуру с дырочкой можно следующим образом.

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
    }
}

Уловка состоит в том, чтобы изменить путь к отверстию. К сожалениюPath не поддерживает (пока) изменение пути прямо из коробки, поэтому расширение (которое использует UIBezierPath). Затем форму можно использовать для отсечения и проверки попадания:

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
    }
}

Основываясь на этой статье , вот .reverseMaskмодификатор, который вы можете использовать вместо .mask. Я изменил его для поддержки iOS 13 и выше.

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

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

Применение:

      ViewToMask()
.reverseMask {
    MaskView()
}

В качестве уточнения ответа @Asperi : если вы не хотите передавать явный размер маске формы, вы можете использовать SwiftUIShapeвведите для достижения того же результата:

      struct ContentView: View {
    var body: some View {
        Rectangle()
            .fill(.black)
            .mask(HoleShape().fill(style: FillStyle(eoFill: true)))
    }
}

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

Я еще не тестировал это, но не могли бы вы сделать что-нибудь вроде этого:

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>) {

    }
}

Применение:

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)
        }
    }
}

Если вам нужно что-то вроде этого:

Затем вы можете просто поместить две фигуры в ZStack и зададим маскирующему элементу цвет фона:

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)
        }
    }
}
Другие вопросы по тегам