繁体   English   中英

SwiftUI 在深色覆盖层中切出孔,用于类似教程的高亮显示

[英]SwiftUI Cut out hole for view in dark overlay for tutorial-like highlighting

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

例如:假设我有三个圆圈...... 三圈无面具

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

到目前为止,这是我想出的。

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

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

为了实现这一点,我必须使用GeometryReader编写一些骇人听闻的代码。 即:在蓝色圆圈的叠加修改器内创建一个几何阅读器并返回清晰的背景。 这允许我检索视图的动态大小和位置。 如果我只是将蓝色圆圈包裹在普通的GeometryReader语句中,它将删除视图的动态大小和 position。

最后,我存储了蓝色圆圈的frame ,并使用它设置了蒙版的frameposition ,从而实现了我想要的效果,即深色叠加层中蓝色圆圈顶部的切口。

综上所述,我收到运行时错误“在视图更新期间修改 state,这将导致未定义的行为。”

此外,方法似乎非常骇人听闻。 理想情况下,我想创建一个单独的框架,我可以在其中定位一个视图,然后添加一个切出特定形状的覆盖视图以突出显示特定视图。

这是上面示例中的代码:

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

你可以这样做。 使用几何尺寸根本不是“hacky”。 圈子的“造型”随心所欲。


//
//  ContentView.swift
//  SelectedCircle
//
//  Created by Allan Garcia on 27/01/23.
//

import SwiftUI

enum SelectedColor {
    case red
    case blue
    case green
    case none
}

struct ContentView: View {
    
    @State private var selectedColor: SelectedColor = .none
    
    var body: some View {
        GeometryReader { geometry in
            let minSize = min(geometry.size.height, geometry.size.width) * 0.5
            let offset = minSize * 1.2
            ZStack {
                Color.white.zIndex(-100) // background
                if selectedColor != .none {
                    Color.black
                        .opacity(0.9)
                        .zIndex(selectedColor != .none ? 50 : 0)
                        .onTapGesture { selectedColor = .none }
                } // Fade out
                Circle()
                    .fill(Color.red)
                    .zIndex(selectedColor != .none && selectedColor == .red ? 100 : 0)
                    .onTapGesture { selectedColor = .red }
                    .offset(y: offset)
                    .frame(width: minSize, height: minSize)
                Circle()
                    .fill(Color.blue)
                    .zIndex(selectedColor != .none && selectedColor == .blue ? 100 : 0)
                    .onTapGesture { selectedColor = .blue }
                    .frame(width: minSize, height: minSize)
                Circle()
                    .fill(Color.green)
                    .zIndex(selectedColor != .none && selectedColor == .green ? 100 : 0)
                    .onTapGesture { selectedColor = .green }
                    .offset(y: -offset)
                    .frame(width: minSize, height: minSize)
            }
            .ignoresSafeArea(.all)
            .frame(width: geometry.size.width, height: geometry.size.height)
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

如果有什么不清楚的地方,请告诉我。 您还可以将 onTapGesture 更改为您想要的任何其他手势。 枚举,如果有意义,可以移动到您的 model,但我坚信是视图的 state,因此它必须存在于视图中。

编辑:这不会修改视图的“行为”,您正在更改 State,这是执行此操作的最快捷方式。 State 中的更改将触发视图的重绘,视图将反映更改的新 State。

EDIT2:发布于: https://github.com/allangarcia/SelectedCircle

我想你想要这样的东西:

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

实现此目的的一种方法是使用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
        }
    }
}

现在我们可以编写一个绘制彩灯的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)
    }
}

这是聚光灯下的视图。 它在Circle上使用blendMode(.destinationOut)将该圆从底层的Color.black中切出,并使用compositingGroup仅包含CircleColor.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()
    }
}

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

最后, ContentView跟踪选择 state 并添加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()
    }
}

请允许我添加另一种方法。 如果负突出显示的大小对于所有视图都可以保持相同,那么您不需要任何GeometryReader

当然,您也可以将单独的尺寸传递给函数。

我将高亮功能打包到一个视图扩展中,可以方便地用作视图修改器。

在此处输入图像描述

struct ContentView: View {
    
    @State private var highlight = 0
    
    var body: some View {
        
        ZStack {
            VStack {
                Spacer()
                Circle()
                    .fill(.blue)
                    .frame(width: 100)
                    .negativeHighlight(enabled: highlight == 0)

                Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi laoreet elementum purus. Interdum et malesuada fames ac ante ipsum primis in faucibus. Cras vel ipsum et risus vulputate auctor non ac ligula.")
                    .padding()
                    .negativeHighlight(enabled: highlight == 1)

                RoundedRectangle(cornerRadius: 10)
                    .fill(.red)
                    .frame(width: 50, height: 80)
                    .negativeHighlight(enabled: highlight == 2)

                HStack {
                    Rectangle()
                        .fill(.green)
                        .frame(width: 100, height: 100)
                        .padding()
                        .negativeHighlight(enabled: highlight == 3)
                    
                    Circle()
                        .fill(.yellow)
                        .frame(width: 100)
                        .negativeHighlight(enabled: highlight == 4)
                }

                Spacer()

                // move to next view
                Button("Show Next") { highlight = (highlight + 1) % 6 }
                .buttonStyle(.borderedProminent)
                .zIndex(2)
            }
        }
    }
}


extension View {
    
    func negativeHighlight(enabled: Bool) -> some View {
        self
            .overlay(
                Color.black.opacity(0.5)
                    .reverseMask {
                        Circle()
                            .fill(.blue)
                            .frame(width: 150)
                    }
                    .frame(width: 10_000, height: 10_000)
                    .opacity(enabled ? 1 : 0)
            )
            .zIndex(enabled ? 1 : 0)
    }
        
    
    @inlinable func reverseMask<Mask: View>(
        alignment: Alignment = .center,
        @ViewBuilder _ mask: () -> Mask
    ) -> some View {
            self.mask(
                ZStack {
                    Rectangle()
                    mask()
                        .blendMode(.destinationOut)
                }
            )
        }
}

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM