我已经写了关于使用 GeometryReader、视图首选项和锚点首选项的详细说明。下面的代码使用了这些概念。有关它们如何工作的更多信息,请查看我发布的这篇文章:https://swiftui-lab.com/communicating-with-the-view-tree-part-1/ https://swiftui-lab.com/communicating-with-the-view-tree-part-1/
下面的解决方案将正确设置下划线的动画:
我努力完成这项工作,我同意你的观点。有时,您只需要能够在层次结构中向上或向下传递一些框架信息。事实上,WWDC2019 会议 237(使用 SwiftUI 构建自定义视图)解释了视图不断传达其大小。它基本上是说父母向孩子建议尺寸,孩子决定他们想要如何布局自己并与父母沟通。他们是怎么做到的?我怀疑anchorPreference与此有关。然而,它非常模糊并且根本没有记录。 API 是公开的,但是要掌握那些长函数原型是如何工作的……我现在没有时间。
我认为 Apple 没有记录这一点是为了迫使我们重新思考整个框架,忘记“旧的”UIKit 习惯并开始声明式思考。然而,有时仍然需要这样做。您有没有想过背景修改器是如何工作的?我很想看到这个实施。它会解释很多!我希望苹果能在不久的将来记录偏好。我一直在尝试自定义 PreferenceKey,它看起来很有趣。
现在回到您的具体需求,我设法解决了。您需要两个尺寸(文本的 x 位置和宽度)。一个我觉得很公平,另一个似乎有点黑客。尽管如此,它工作得很好。
文本的 x 位置我通过创建自定义水平对齐方式解决了这个问题。有关该检查会话 237 的更多信息(19:00 分钟)。尽管我建议您观看整个内容,但它可以让您了解布局过程的工作原理。
然而,我并不为宽度感到自豪......;-) 它需要 DispatchQueue 以避免在显示时更新视图。更新:我在下面的第二个实现中修复了它
首次实施
extension HorizontalAlignment {
private enum UnderlineLeading: AlignmentID {
static func defaultValue(in d: ViewDimensions) -> CGFloat {
return d[.leading]
}
}
static let underlineLeading = HorizontalAlignment(UnderlineLeading.self)
}
struct GridViewHeader : View {
@State private var activeIdx: Int = 0
@State private var w: [CGFloat] = [0, 0, 0, 0]
var body: some View {
return VStack(alignment: .underlineLeading) {
HStack {
Text("Tweets").modifier(MagicStuff(activeIdx: $activeIdx, widths: $w, idx: 0))
Spacer()
Text("Tweets & Replies").modifier(MagicStuff(activeIdx: $activeIdx, widths: $w, idx: 1))
Spacer()
Text("Media").modifier(MagicStuff(activeIdx: $activeIdx, widths: $w, idx: 2))
Spacer()
Text("Likes").modifier(MagicStuff(activeIdx: $activeIdx, widths: $w, idx: 3))
}
.frame(height: 50)
.padding(.horizontal, 10)
Rectangle()
.alignmentGuide(.underlineLeading) { d in d[.leading] }
.frame(width: w[activeIdx], height: 2)
.animation(.linear)
}
}
}
struct MagicStuff: ViewModifier {
@Binding var activeIdx: Int
@Binding var widths: [CGFloat]
let idx: Int
func body(content: Content) -> some View {
Group {
if activeIdx == idx {
content.alignmentGuide(.underlineLeading) { d in
DispatchQueue.main.async { self.widths[self.idx] = d.width }
return d[.leading]
}.onTapGesture { self.activeIdx = self.idx }
} else {
content.onTapGesture { self.activeIdx = self.idx }
}
}
}
}
更新:不使用 DispatchQueue 更好的实现
我的第一个解决方案有效,但我对宽度传递到下划线视图的方式并不太自豪。
我找到了一种更好的方法来实现同样的目标。事实证明,背景修改器非常强大。它不仅仅是一个可以让您装饰视图背景的修改器。
基本步骤是:
- Use
Text("text").background(TextGeometry())
。 TextGeometry 是一个自定义视图,其父级与文本视图大小相同。这就是 .background() 的作用。很强大。
- 在我的实施中文本几何我使用 GeometryReader 来获取父级的几何图形,这意味着我获取了 Text 视图的几何图形,这意味着我现在有了宽度。
- 现在要传递宽度回来,我正在使用优先。关于它们的文档为零,但经过一些实验后,我认为如果您愿意的话,首选项类似于“查看属性”。我创建了我的自定义偏好键,称为宽度偏好键我在 TextGeometry 中使用它来将宽度“附加”到视图,以便可以在层次结构中更高的位置读取它。
- 回到祖先,我使用偏好改变检测宽度的变化,并相应地设置宽度数组。
这听起来可能太复杂了,但代码最好地说明了这一点。这是新的实现:
import SwiftUI
extension HorizontalAlignment {
private enum UnderlineLeading: AlignmentID {
static func defaultValue(in d: ViewDimensions) -> CGFloat {
return d[.leading]
}
}
static let underlineLeading = HorizontalAlignment(UnderlineLeading.self)
}
struct WidthPreferenceKey: PreferenceKey {
static var defaultValue = CGFloat(0)
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = nextValue()
}
typealias Value = CGFloat
}
struct GridViewHeader : View {
@State private var activeIdx: Int = 0
@State private var w: [CGFloat] = [0, 0, 0, 0]
var body: some View {
return VStack(alignment: .underlineLeading) {
HStack {
Text("Tweets")
.modifier(MagicStuff(activeIdx: $activeIdx, idx: 0))
.background(TextGeometry())
.onPreferenceChange(WidthPreferenceKey.self, perform: { self.w[0] = $0 })
Spacer()
Text("Tweets & Replies")
.modifier(MagicStuff(activeIdx: $activeIdx, idx: 1))
.background(TextGeometry())
.onPreferenceChange(WidthPreferenceKey.self, perform: { self.w[1] = $0 })
Spacer()
Text("Media")
.modifier(MagicStuff(activeIdx: $activeIdx, idx: 2))
.background(TextGeometry())
.onPreferenceChange(WidthPreferenceKey.self, perform: { self.w[2] = $0 })
Spacer()
Text("Likes")
.modifier(MagicStuff(activeIdx: $activeIdx, idx: 3))
.background(TextGeometry())
.onPreferenceChange(WidthPreferenceKey.self, perform: { self.w[3] = $0 })
}
.frame(height: 50)
.padding(.horizontal, 10)
Rectangle()
.alignmentGuide(.underlineLeading) { d in d[.leading] }
.frame(width: w[activeIdx], height: 2)
.animation(.linear)
}
}
}
struct TextGeometry: View {
var body: some View {
GeometryReader { geometry in
return Rectangle().fill(Color.clear).preference(key: WidthPreferenceKey.self, value: geometry.size.width)
}
}
}
struct MagicStuff: ViewModifier {
@Binding var activeIdx: Int
let idx: Int
func body(content: Content) -> some View {
Group {
if activeIdx == idx {
content.alignmentGuide(.underlineLeading) { d in
return d[.leading]
}.onTapGesture { self.activeIdx = self.idx }
} else {
content.onTapGesture { self.activeIdx = self.idx }
}
}
}
}