我已经写了关于使用 GeometryReader、视图首选项和锚首选项的详细说明。下面的代码使用了这些概念。有关它们如何工作的更多信息,请查看我发布的这篇文章:https://swiftui-lab.com/communicating-with-the-view-tree-part-1/
下面的解决方案将正确地为下划线设置动画:
我努力完成这项工作,我同意你的看法。有时,您只需要能够向上或向下传递层次结构,一些框架信息。事实上,WWDC2019 session 237(Building Custom Views with 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 的情况下更好地实现
我的第一个解决方案有效,但我对将宽度传递给下划线视图的方式并不太自豪。
我找到了实现相同目标的更好方法。事实证明,background 修饰符非常强大。它不仅仅是一个可以让您装饰视图背景的修饰符。
基本步骤是:
- 使用
Text("text").background(TextGeometry())。 TextGeometry 是一个自定义视图,其父视图的大小与文本视图相同。这就是 .background() 的作用。非常强大。
- 在我的 TextGeometry 实现中,我使用 GeometryReader 来获取父级的几何图形,这意味着,我得到了 Text 视图的几何图形,这意味着我现在有了宽度。
- 现在要传递宽度,我正在使用 Preferences。关于它们的文档为零,但经过一些实验,我认为偏好类似于“查看属性”,如果你愿意的话。我创建了我的自定义 PreferenceKey,称为 WidthPreferenceKey,并在 TextGeometry 中使用它来将宽度“附加”到视图,因此可以在层次结构中读取更高的位置。李>
- 回到祖先,我使用 onPreferenceChange 来检测宽度的变化,并相应地设置宽度数组。
这听起来可能太复杂了,但代码最能说明问题。这是新的实现:
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 }
}
}
}
}