SwiftUI 在黑暗覆盖层中切出孔以进行类似教程的突出显示

Jar*_*red 5 masking swiftui geometryreader

我正在尝试在 SwiftUI 中创建一个教程框架,它可以找到特定的视图并通过使屏幕的其余部分变暗来突出显示它。

例如:假设我有三个圆圈...... 三圈无口罩

我想强调蓝色的...... 突出显示的蓝色圆圈

这是我到目前为止所想到的。

  • 创建一个 ZStack。
  • 将半透明黑色背景放在上面。
  • 将倒置蒙版添加到背景中以在其中打孔以显示蓝色圆圈。

这可行,但我需要蓝色圆圈的大小和位置,以便知道在哪里放置遮罩。

为了实现这一目标,我必须使用GeometryReader. 即:在蓝色圆圈的覆盖修改器内创建一个几何读取器并返回清晰的背景。这允许我检索视图的动态大小和位置。如果我只是将蓝色圆圈包裹在普通GeometryReader语句中,它将删除视图的动态大小和位置。

最后,我存储frame蓝色圆圈的 ,并使用它设置蒙版的frameposition,从而实现我想要的,在黑暗覆盖层中蓝色圆圈顶部的切口。

话虽如此,我收到了运行时错误“在视图更新期间修改状态,这将导致未定义的行为。”

而且方法似乎非常黑客和粘性。理想情况下,我想创建一个单独的框架,在其中可以定位视图,然后添加具有特定形状剪切的覆盖视图,以突出显示特定视图。

这是上面示例中的代码:

@State var blueFrame: CGRect = .zero

var body: some View {
    
    ZStack {
        VStack {
            Circle()
                .fill(Color.red)
                .frame(width: 100, height: 100)
            
            ZStack {
                Circle()
                    .fill(Color.blue)
                    .frame(width: 100, height: 100)
                    .overlay {
                        GeometryReader { geometry -> Color in
                            
                            let geoFrame = geometry.frame(in: .global)
                            blueFrame = CGRect(x: geoFrame.origin.x + (geoFrame.width / 2),
                                                  y: geoFrame.origin.y + (geoFrame.height / 2),
                                                  width: geoFrame.width,
                                                  height: geoFrame.height)
                            
                            return Color.clear
                        }
                    }
            }
            
            Circle()
                .fill(Color.green)
                .frame(width: 100, height: 100)
        }
        
        Color.black.opacity(0.75)
            .edgesIgnoringSafeArea(.all)
            .reverseMask {
                Circle()
                    .frame(width: blueFrame.width + 10, height: blueFrame.height + 10)
                    .position(blueFrame.origin)

            }
            .ignoresSafeArea()
    }
}
Run Code Online (Sandbox Code Playgroud)

rob*_*off 8

我想你想要这样的东西:

三个圆圈堆叠在一起。 顶部圆圈是红色的。 中间的圆圈是黄色的。 底部圆圈是绿色的。 堆栈下方是一个分段选择器,其中包含“无”、“红色”、“黄色”和“绿色”段。 最初选择“无”段。 然后我选择“红色”部分。 除了红色圆圈周围的聚光灯区域外,圆圈堆栈变暗。 然后我单击“绿色”,聚光灯将移动到绿色圆圈。 我单击“黄色”,聚光灯将移动到黄色圆圈。 我单击“无”,变暗就会消失。 我单击“红色”,当聚光灯动画到红色圆圈时,调光又回来了。

实现此目的的一种方法是将matchedGeometryEffect聚光灯放在选定的灯光上,并使用blendModecompositingGroup切割变暗覆盖层中的孔。

首先,让我们定义一个类型来跟踪选择的灯光:

enum Light: Hashable, CaseIterable {
    case red
    case yellow
    case green

    var color: Color {
        switch self {
        case .red: return .red
        case .yellow: return .yellow
        case .green: return .green
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

现在我们可以编写一个View绘制彩色灯光的程序。每个灯光都经过修改,matchedGeometryEffect使其框架可供聚光灯视图使用(稍后编写)。

struct LightsView: View {
    let namespace: Namespace.ID

    var body: some View {
        VStack(spacing: 20) {
            ForEach(Light.allCases, id: \.self) { light in
                Circle()
                    .foregroundColor(light.color)
                    .matchedGeometryEffect(
                        id: light, in: namespace,
                        properties: .frame, anchor: .center,
                        isSource: true
                    )
            }
        }
        .padding(20)
    }
}
Run Code Online (Sandbox Code Playgroud)

这是聚光灯视图。它使用blendMode(.destinationOut)aCircle将圆从底层中切掉Color.black,并使用compositingGroup将混合包含在Circle和 中Color.black

struct SpotlightView: View {
    var spotlitLight: Light
    var namespace: Namespace.ID

    var body: some View {
        ZStack {
            Color.black
            Circle()
                .foregroundColor(.white)
                .blur(radius: 4)
                .padding(-10)
                .matchedGeometryEffect(
                    id: spotlitLight, in: namespace,
                    properties: .frame, anchor: .center,
                    isSource: false
                )
                .blendMode(.destinationOut)
        }
        .compositingGroup()
    }
}
Run Code Online (Sandbox Code Playgroud)

在 中HighlightingView,将 放在SpotlightView之上LightsView并为其设置动画SpotlightView

struct HighlightingView: View {
    var spotlitLight: Light
    var isSpotlighting: Bool
    @Namespace private var namespace

    var body: some View {
        ZStack {
            LightsView(namespace: namespace)

            SpotlightView(
                spotlitLight: spotlitLight,
                namespace: namespace
            )
            .opacity(isSpotlighting ? 0.5 : 0)
            .animation(
                .easeOut,
                value: isSpotlighting ? spotlitLight : nil
            )
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

最后,ContentView跟踪选择状态并添加Picker

struct ContentView: View {
    @State var isSpotlighting = false
    @State var spotlitLight: Light = .red

    private var selection: Binding<Light?> {
        Binding(
            get: { isSpotlighting ? spotlitLight : nil },
            set: {
                if let light = $0 {
                    isSpotlighting = true
                    spotlitLight = light
                } else {
                    isSpotlighting = false
                }
            }
        )
    }

    var body: some View {
        VStack {
            HighlightingView(
                spotlitLight: spotlitLight,
                isSpotlighting: isSpotlighting
            )

            Picker("Light", selection: selection) {
                Text("none").tag(Light?.none)
                ForEach(Light.allCases, id: \.self) {
                    Text("\($0)" as String)
                        .tag(Optional($0))
                }
            }
            .pickerStyle(.segmented)
        }
        .padding()
    }
}
Run Code Online (Sandbox Code Playgroud)