提问人:fasoh 提问时间:10/17/2023 更新时间:10/17/2023 访问量:65
如何在弹出 Overlay Highlight 的 VStack 中创建 SwiftUI 视图
How to Create a SwiftUI View in a VStack That Pops Out with Overlay Highlight
问:
请考虑以下简化代码片段
import SwiftUI
struct ContentView: View {
var body: some View {
ScrollView {
// Some other views here that should also be covered
VStack {
ForEach(0..<8) { i in
ListElement()
.padding(.horizontal)
}
}
}
}
}
struct ListElement: View {
@State private var isFocused: Bool = false
var body: some View {
Button(action: {
self.isFocused.toggle()
}, label: {
Text(isFocused ? "I am focused" : "I am not focused")
.foregroundColor(.white)
.frame(maxWidth: .infinity)
.padding()
.background(.gray)
.background(
isFocused ?
Color.pink
.opacity(0.5)
.frame(width: 10000, height: 10000)
.ignoresSafeArea()
.onTapGesture {
self.isFocused = false
}
: nil
)
})
}
}
#Preview {
ContentView()
}
其结果如下
通常,ScrollView 包含更多的 ListElements。 如果用户点击一个 ListElement,我希望它调整大小并显示其他信息,这是一个简单的按钮。 但是,我也希望除 ListElement 之外的所有内容都具有透明的覆盖层,以便更加强调选定的 ListElement 并淡化其他所有内容。 如果用户点击除所选 ListElement(在本例中为其背景)以外的任何内容,则不应再聚焦相应的 ListElement。 有一些解决方案(例如类似“教程”的视图),但据我所知,由于依赖于 GeometryReader,因此在 ScrollViews 中不起作用。
在示例代码中,我尝试在每个 ListElement 上使用一个大得离谱的 -modifier 来实现这一点,该修饰符将覆盖整个屏幕。
它看起来像这样:background
万岁!VStack 在视觉上被淡化,选定的 ListElement 清晰地处于焦点中。但是,如果选择了除最后一个 ListElement 之外的任何一个,则会发生以下情况:
如您所见,-modifier 不覆盖分层选中的 ListElement 之后的任何 ListElement。我认为这是由于 SwiftUI 中的视图层次结构。有没有办法将当前选择的 ListElement 推送到层次结构的顶部,以便我可以覆盖它后面的任何其他内容?background
答:
要回答您的问题,您必须向 ListElement 的按钮添加修饰符。但是您还必须将焦点状态存储在全局位置,因为 - 我猜 - 如果一个列表元素是焦点的,我们应该从先前焦点元素(如果有的话)中删除焦点。.zIndex(isFocused ? 1 : -1)
因此,代码如下所示:
import SwiftUI
import Foundation
struct ContentView: View {
@State var focusedIndex: Int?
var body: some View {
ScrollView {
Text("Some other views here that should also be covered")
VStack {
ForEach(0..<8) { i in
ListElement(
isFocused: focusedIndex == i,
action: { focusedIndex = i }
)
.padding(.horizontal)
}
}
}
}
}
struct ListElement: View {
let isFocused: Bool
let action: () -> Void
var body: some View {
Button(action: {
action()
}, label: {
Text(isFocused ? "I am focused" : "I am not focused")
.foregroundColor(.white)
.frame(maxWidth: .infinity)
.padding()
.background(.gray)
.background(
isFocused ?
Color.pink
.opacity(0.5)
.frame(width: 10000, height: 10000)
.ignoresSafeArea()
: nil
)
})
.zIndex(isFocused ? 1 : -1)
}
}
#Preview {
ContentView()
}
虽然它有效,但我对这个解决方案并不完全满意。我觉得它很笨拙,我没有 100% 的信心它会在每种情况下都起作用。
因此,我认为更好的解决方案是在完整内容上方画一个“封面”,并在特定位置切一个洞,使屏幕的一部分可见。为此,我们必须确定给定元素在屏幕中的确切位置,然后我们必须绘制封面并使用“反向掩码”在该位置切一个洞。 该解决方案具有一些优点,例如它不仅适用于列表元素,还适用于屏幕上的任何类型的视图。我们也可以将其扩展到“切割”不是矩形,而是任何形状的孔。
这个解决方案还有一个未解决的问题:如果你有很多元素,假设你滚动你的内容,那么孔的位置将是不正确的。如果您检查滚动视图的 contentOffset 并从测量的 CGRect 的 origin.x 中减去滚动视图的 offset.x,则可以解决它。 我没有实现它,因为我的代码太复杂了,但您可以使用它作为帮助轻松扩展此代码。
import SwiftUI
struct ContentView: View {
@State var focusedRect: CGRect?
@State var focusedIndex: Int?
var body: some View {
TutorialContent(
highlightRect: focusedRect,
action: {
focusedRect = nil
focusedIndex = nil
},
content: {
ScrollView {
VStack {
VStack {
Text("Another view which will be covered!")
}
ForEach(0 ..< 8) { i in
let isFocused = i == focusedIndex
ListElement(
text: isFocused ? "I am focused" : "I am not focused",
action: { rect in
focusedRect = rect
focusedIndex = i
}
)
.padding(.horizontal)
}
}
}
}
)
.animation(.default, value: focusedIndex)
}
}
struct ListElement: View {
let text: String
let action: (CGRect) -> Void
@State var rect: CGRect = .zero
var body: some View {
Button(
action: {
action(rect)
},
label: {
Text(text)
.padding()
.foregroundColor(.white)
.frame(maxWidth: .infinity)
.background(.gray)
})
.measure { rect = $0 }
}
}
struct TutorialContent<Content: View>: View {
let highlightRect: CGRect?
let action: () -> Void
let content: () -> Content
var body: some View {
ZStack {
if let rect = highlightRect {
ZStack {
Color.pink.opacity(0.5)
Rectangle()
.frame(
width: rect.width,
height: rect.height
)
.position(CGPoint(x: rect.midX, y: rect.midY))
.blendMode(.destinationOut)
}
.ignoresSafeArea(.all)
.compositingGroup()
.zIndex(highlightRect != nil ? 1 : -1)
.onTapGesture {
action()
}
}
content()
}
}
}
extension View {
@ViewBuilder func measure(
in coordinateSpace: CoordinateSpace = .global,
_ block: @escaping (CGRect) -> Void
) -> some View {
MeasuredView(coordinateSpace: coordinateSpace, onMeasure: block, content: self)
}
}
struct MeasuredView<Content: View>: View {
let coordinateSpace: CoordinateSpace
let onMeasure: (CGRect) -> Void
let content: Content
var body: some View {
content
.overlay {
GeometryReader { proxy in
Rectangle().fill(.clear)
.task {
onMeasure(proxy.frame(in: coordinateSpace))
}
}
}
}
}
#Preview {
ContentView()
}
评论