[英]SwiftUI Cut out hole for view in dark overlay for tutorial-like highlighting
我试图在 SwiftUI 中创建一个教程框架,它找到一个特定的视图并通过使屏幕的 rest 变暗来突出显示它。
到目前为止,这是我想出的。
这可行,但我需要蓝色圆圈的大小和位置,以便知道将面罩放在哪里。
为了实现这一点,我必须使用GeometryReader
编写一些骇人听闻的代码。 即:在蓝色圆圈的叠加修改器内创建一个几何阅读器并返回清晰的背景。 这允许我检索视图的动态大小和位置。 如果我只是将蓝色圆圈包裹在普通的GeometryReader
语句中,它将删除视图的动态大小和 position。
最后,我存储了蓝色圆圈的frame
,并使用它设置了蒙版的frame
和position
,从而实现了我想要的效果,即深色叠加层中蓝色圆圈顶部的切口。
综上所述,我收到运行时错误“在视图更新期间修改 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。
我想你想要这样的东西:
实现此目的的一种方法是使用matchedGeometryEffect
将聚光灯放在选定的灯光上,并使用blendMode
和compositingGroup
在变暗的叠加层中切出洞。
首先,让我们定义一个类型来跟踪选择了哪个灯:
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
仅包含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()
}
}
在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.