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 (на примере, чтобы сделать отверстие в поле зрения)
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)
}
}
}