Ответ 1
Я написал подробное объяснение об использовании GeometryReader, настройках просмотра и привязках. Код ниже использует эти понятия. Дополнительную информацию о том, как они работают, смотрите в этой статье, которую я разместил: https://swiftui-lab.com/communicating-with-the-view-tree-part-1/
Решение, приведенное ниже, будет правильно анимировать подчеркивание:
Я изо всех сил пытался сделать эту работу, и я согласен с вами. Иногда вам просто нужно пройти вверх или вниз по иерархии, некоторую информацию о кадрировании. Фактически, сеанс 237 WWDC2019 (Создание пользовательских представлений с помощью SwiftUI) объясняет, что представления непрерывно сообщают свои размеры. Это в основном говорит, что Родитель предлагает размер ребенку, дети решают, как они хотят расположить себя и общаться с родителем. Как они это делают? Я подозреваю, что anchorPreference как-то связан с этим. Однако это очень неясно и пока не документировано. API раскрывается, но понимает, как работают эти прототипы длинных функций... что, черт возьми, у меня сейчас нет времени.
Я думаю, что Apple оставила это недокументированным, чтобы заставить нас переосмыслить всю структуру и забыть о "старых" привычках UIKit и начать мыслить декларативно. Однако бывают времена, когда это необходимо. Вы когда-нибудь задумывались, как работает модификатор фона? Я хотел бы увидеть эту реализацию. Это многое бы объяснило! Я надеюсь, что Apple документирует предпочтения в ближайшем будущем. Я экспериментировал с пользовательским PreferenceKey, и он выглядит интересно.
Теперь вернемся к вашей конкретной потребности, мне удалось решить это. Вам нужны два измерения (позиция х и ширина текста). Один мне кажется честным и откровенным, другой кажется чем-то вроде взлома. Тем не менее, это работает отлично.
Положение х текста я решил, создав собственное горизонтальное выравнивание. Больше информации о том сеансе проверки 237 (в минуту 19:00). Хотя я рекомендую вам посмотреть все это, оно проливает много света на то, как работает процесс верстки.
Однако шириной я не очень горжусь... ;-) Требуется DispatchQueue, чтобы избежать обновления вида во время отображения. ОБНОВЛЕНИЕ: я исправил это во второй реализации ниже
Первая реализация
import SwiftUI
extension HorizontalAlignment {
private enum UnderlineLeading: AlignmentID {
static func defaultValue(in d: ViewDimensions) -> Length {
return d[.leading]
}
}
static let underlineLeading = HorizontalAlignment(UnderlineLeading.self)
}
struct GridViewHeader : View {
@State private var activeIdx: Int = 0
@State private var w: [Length] = [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(.basic())
}
}
}
struct MagicStuff: ViewModifier {
@Binding var activeIdx: Int
@Binding var widths: [Length]
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]
}.tapAction { self.activeIdx = self.idx }
} else {
content.tapAction { self.activeIdx = self.idx }
}
}
}
}
Обновление: лучшая реализация без использования DispatchQueue
Мое первое решение работает, но я не слишком гордился тем, как ширина передается в подчеркивание.
Я нашел лучший способ достичь того же. Оказывается, модификатор background очень мощный. Это гораздо больше, чем модификатор, который позволяет вам украшать фон представления.
Основные шаги:
- Используйте
Text("text").background(TextGeometry())
. TextGeometry - это пользовательское представление, родительский элемент которого имеет тот же размер, что и текстовое представление. Это то, что делает .background(). Очень мощный. - В моей реализации TextGeometry я использую GeometryReader, чтобы получить геометрию родителя, что означает, что я получил геометрию представления Text, что означает, что теперь у меня есть ширина.
- Теперь, чтобы передать ширину, я использую Настройки. Там нет документации о них, но после небольшого эксперимента, я думаю, предпочтения - это что-то вроде "атрибутов вида", если хотите. Я создал свой собственный PreferenceKey, который называется WidthPreferenceKey, и я использую его в TextGeometry для "прикрепления" ширины к представлению, чтобы его можно было прочитать выше в иерархии.
- Вернувшись к предку, я использую onPreferenceChange для обнаружения изменений в ширине и соответствующим образом устанавливаю массив widths.
Все это может показаться слишком сложным, но код иллюстрирует это лучше всего. Вот новая реализация:
import SwiftUI
extension HorizontalAlignment {
private enum UnderlineLeading: AlignmentID {
static func defaultValue(in d: ViewDimensions) -> Length {
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: [Length] = [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(.basic())
}
}
}
struct TextGeometry: View {
var body: some View {
GeometryReader { geometry in
return Rectangle().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]
}.tapAction { self.activeIdx = self.idx }
} else {
content.tapAction { self.activeIdx = self.idx }
}
}
}
}