web-dev-qa-db-fra.com

Comment faire pour afficher la taille d'une autre vue dans SwiftUI

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()
            }
        }
    }
}

27
Zach Fuller

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:

enter image description here

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

Première implémentation

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 }
            }
        }
    }
}

Mise à jour: meilleure implémentation sans utiliser DispatchQueue

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:

  1. Utilisez 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.
  2. Dans mon implémentation de TextGeometry j'utilise GeometryReader, pour obtenir la géométrie du parent, ce qui signifie, j'obtiens la géométrie de la vue Text, ce qui signifie que je ont maintenant la largeur.
  3. Maintenant, pour passer la largeur, j'utilise Préférences . Il n'y a aucune documentation à leur sujet, mais après un peu d'expérimentation, je pense que les préférences sont quelque chose comme "voir les attributs" si vous le souhaitez. J'ai créé mon PreferenceKey personnalisé , appelé WidthPreferenceKey et je l'utilise dans TextGeometry pour "attacher" la largeur à la vue, afin qu'elle puisse être lue plus haut dans la hiérarchie.
  4. De retour dans l'ancêtre, j'utilise onPreferenceChange pour détecter les changements de largeur et définir le tableau des largeurs en conséquence.

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 }
            }
        }
    }
}
26
kontiki

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)

            }
        }
    }
}

Et voici à quoi cela ressemble dans l'aperçu: Preview screen

2
piebie

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:

screenshot

0
Ricky Padilla

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")
}
0
rust

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 })
            }
        }
    }
}
0
Paul B