J'essaie de recréer une partie de l'application iOS Twitter pour apprendre SwiftUI et je me demande comment changer dynamiquement la largeur d'une vue pour qu'elle soit la largeur d'une autre vue. Dans mon cas, le soulignement doit avoir la même largeur que la vue Texte.
J'ai joint une capture d'écran pour essayer de mieux expliquer à quoi je fais référence. Toute aide serait grandement appréciée, merci!
Voici également le code que j'ai jusqu'à présent:
import SwiftUI
struct GridViewHeader : View {
@State var leftPadding: Length = 0.0
@State var underLineWidth: Length = 100
var body: some View {
return VStack {
HStack {
Text("Tweets")
.tapAction {
self.leftPadding = 0
}
Spacer()
Text("Tweets & Replies")
.tapAction {
self.leftPadding = 100
}
Spacer()
Text("Media")
.tapAction {
self.leftPadding = 200
}
Spacer()
Text("Likes")
}
.frame(height: 50)
.padding(.horizontal, 10)
HStack {
Rectangle()
.frame(width: self.underLineWidth, height: 2, alignment: .bottom)
.padding(.leading, leftPadding)
.animation(.basic())
Spacer()
}
}
}
}
J'ai écrit une explication détaillée sur l'utilisation de GeometryReader, des préférences d'affichage et des préférences d'ancrage. Le code ci-dessous utilise ces concepts. Pour plus d'informations sur leur fonctionnement, consultez cet article que j'ai publié: https://swiftui-lab.com/communicating-with-the-view-tree-part-1/
La solution ci-dessous animera correctement le soulignement:
J'ai eu du mal à faire fonctionner cela et je suis d'accord avec vous. Parfois, il vous suffit de pouvoir remonter ou descendre la hiérarchie, certaines informations de cadrage. En fait, la session WWDC2019 237 (Création de vues personnalisées avec SwiftUI), explique que les vues communiquent leur dimensionnement en continu. Il dit essentiellement que Parent propose la taille à l'enfant, les enfants décident comment ils veulent se mettre en page et communiquer avec le parent. Comment font-ils cela? Je soupçonne que l'ancrePreference a quelque chose à voir avec cela. Cependant, il est très obscur et pas encore documenté du tout. L'API est exposée, mais comprendre comment fonctionnent ces prototypes à longue fonction ... c'est un enfer pour lequel je n'ai pas le temps pour l'instant.
Je pense que Apple a laissé cela sans papiers pour nous forcer à repenser le cadre entier et à oublier les "vieilles" habitudes UIKit et commencer à penser de manière déclarative. Cependant, il y a encore des moments où cela est nécessaire. Avez-vous déjà me demande comment fonctionne le modificateur d'arrière-plan? J'aimerais voir cette implémentation. Cela expliquerait beaucoup! J'espère Apple documentera les préférences dans un avenir proche. J'ai expérimenté avec PreferenceKey personnalisé et ça a l'air intéressant.
Maintenant, revenons à votre besoin spécifique, j'ai réussi à le résoudre. Il y a deux dimensions dont vous avez besoin (la position x et la largeur du texte). L'un je suis juste et carré, l'autre semble un peu un hack. Néanmoins, cela fonctionne parfaitement.
La position x du texte, je l'ai résolu en créant un alignement horizontal personnalisé. Plus d'informations sur cette session de vérification 237 (à la minute 19:00). Bien que je vous recommande de regarder le tout, cela donne beaucoup de lumière sur le fonctionnement du processus de mise en page.
La largeur, cependant, dont je ne suis pas si fier ... ;-) Il faut DispatchQueue pour éviter de mettre à jour la vue lors de l'affichage. MISE À JOUR: je l'ai corrigé dans la deuxième implémentation ci-dessous
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 }
}
}
}
}
Ma première solution fonctionne, mais je n'étais pas trop fier de la façon dont la largeur est passée à la vue soulignée.
J'ai trouvé une meilleure façon de réaliser la même chose. Il s'avère que le modificateur d'arrière-plan est très puissant. C'est bien plus qu'un modificateur qui peut vous permettre de décorer l'arrière-plan d'une vue.
Les étapes de base sont les suivantes:
Text("text").background(TextGeometry())
. TextGeometry est une vue personnalisée qui a un parent de la même taille que la vue texte. C'est ce que fait .background (). Très puissant.Cela peut sembler trop complexe, mais le code l'illustre mieux. Voici la nouvelle implémentation:
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 }
}
}
}
}
Essayez ceci:
import SwiftUI
var titles = ["Tweets", "Tweets & Replies", "Media", "Likes"]
struct GridViewHeader : View {
@State var selectedItem: String = "Tweets"
var body: some View {
HStack(spacing: 20) {
ForEach(titles.identified(by: \.self)) { title in
HeaderTabButton(title: title, selectedItem: self.$selectedItem)
}
.frame(height: 50)
}.padding(.horizontal, 10)
}
}
struct HeaderTabButton : View {
var title: String
@Binding var selectedItem: String
var isSelected: Bool {
selectedItem == title
}
var body: some View {
VStack {
Button(action: { self.selectedItem = self.title }) {
Text(title).fixedSize(horizontal: true, vertical: false)
Rectangle()
.frame(height: 2, alignment: .bottom)
.relativeWidth(1)
.foregroundColor(isSelected ? Color.accentColor : Color.clear)
}
}
}
}
Voici une solution super simple, même si elle ne tient pas compte des onglets étirés sur toute la largeur - mais cela ne devrait être qu'un calcul supplémentaire pour le calcul du rembourrage.
import SwiftUI
struct HorizontalTabs: View {
private let tabsSpacing = CGFloat(16)
private func tabWidth(at index: Int) -> CGFloat {
let label = UILabel()
label.text = tabs[index]
let labelWidth = label.intrinsicContentSize.width
return labelWidth
}
private var leadingPadding: CGFloat {
var padding: CGFloat = 0
for i in 0..<tabs.count {
if i < selectedIndex {
padding += tabWidth(at: i) + tabsSpacing
}
}
return padding
}
let tabs: [String]
@State var selectedIndex: Int = 0
var body: some View {
VStack(alignment: .leading) {
HStack(spacing: tabsSpacing) {
ForEach(0..<tabs.count, id: \.self) { index in
Button(action: { self.selectedIndex = index }) {
Text(self.tabs[index])
}
}
}
Rectangle()
.frame(width: tabWidth(at: selectedIndex), height: 3, alignment: .bottomLeading)
.foregroundColor(.blue)
.padding(.leading, leadingPadding)
.animation(Animation.spring())
}
}
}
HorizontalTabs(tabs: ["one", "two", "three"])
rend ceci:
Il vous suffit de spécifier un cadre avec une hauteur à l'intérieur. Voici un exemple:
VStack {
Text("First Text Label")
Spacer().frame(height: 50) // This line
Text("Second Text Label")
}
Permettez-moi de suggérer modestement une légère modification de cette réponse brillante : Version sans utiliser les préférences:
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 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 {
var w: CGFloat = 0
return Group {
if activeIdx == idx {
content.alignmentGuide(.underlineLeading) { d in
w = d.width
return d[.leading]
}.onTapGesture { self.activeIdx = self.idx }.onAppear(perform: {self.widths[self.idx] = w})
} else {
content.onTapGesture { self.activeIdx = self.idx }
}
}
}
}
Version utilisant les préférences et GeometryReader
:
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, widthStorage: $w))
Spacer()
Text("Tweets & Replies")
.modifier(MagicStuff(activeIdx: $activeIdx, idx: 1, widthStorage: $w))
Spacer()
Text("Media")
.modifier(MagicStuff(activeIdx: $activeIdx, idx: 2, widthStorage: $w))
Spacer()
Text("Likes")
.modifier(MagicStuff(activeIdx: $activeIdx, idx: 3, widthStorage: $w))
}
.frame(height: 50)
.padding(.horizontal, 10)
Rectangle()
.frame(width: w[activeIdx], height: 2)
.animation(.linear)
}
}
}
struct MagicStuff: ViewModifier {
@Binding var activeIdx: Int
let idx: Int
@Binding var widthStorage: [CGFloat]
func body(content: Content) -> some View {
Group {
if activeIdx == idx {
content.background(GeometryReader { geometry in
return Color.clear.preference(key: WidthPreferenceKey.self, value: geometry.size.width)
})
.alignmentGuide(.underlineLeading) { d in
return d[.leading]
}.onTapGesture { self.activeIdx = self.idx }
.onPreferenceChange(WidthPreferenceKey.self, perform: { self.widthStorage[self.idx] = $0 })
} else {
content.onTapGesture { self.activeIdx = self.idx }.onPreferenceChange(WidthPreferenceKey.self, perform: { self.widthStorage[self.idx] = $0 })
}
}
}
}