Pour une petite rangée d’étiquettes dans une app Mac, utilisez ViewThatFits pour choisir le préfixe complet le plus long qui tient, en incluant le nombre d’éléments masqués. Ce tutoriel construit cette rangée sans tronquer les étiquettes ni mettre à jour l’état pendant la mise en page.

La tâche concrète consiste à afficher six étiquettes synthétiques dans une fenêtre macOS redimensionnable. Conservez leur ordre, affichez uniquement des étiquettes entières et ajoutez un badge tel que 3 de plus pour le suffixe omis. La solution suppose qu’il y a assez d’espace horizontal pour le badge de comptage seul et assez d’espace vertical pour une rangée.

Choisir des variantes de rangée complètes

Apple décrit ViewThatFits comme un outil qui sélectionne le premier enfant dont la taille idéale tient dans la taille proposée sur les axes contraints. Il est disponible à partir de macOS 13. Utilisez .horizontal ici, car la tâche porte sur la largeur de la rangée ; l’interface environnante doit fournir la hauteur de celle-ci. ViewThatFits

Pour six étiquettes, fournissez sept variantes dans cet ordre : les six étiquettes sans badge, cinq étiquettes plus 1 de plus, quatre plus 2 de plus, et ainsi de suite, jusqu’à la variante contenant uniquement 6 de plus. Chaque variante constitue une rangée complète. Son badge participe à la décision d’ajustement : l’ajout du décompte ne risque donc pas de déplacer inopinément la dernière étiquette visible.

Cette méthode applique une politique de préfixe : une première étiquette longue peut entraîner l’omission de toutes les étiquettes, même si une étiquette courte située plus loin tiendrait. C’est intentionnel. Si votre tâche autorise plutôt à ignorer certaines étiquettes individuellement, définissez cette autre politique de sélection avant de l’implémenter.

Appliquez fixedSize() à chaque rangée candidate afin que ses étiquettes et son badge conservent leurs dimensions idéales. Apple explique que ce modificateur peut permettre à une vue de dépasser les limites de son parent ; à lui seul, il ne garantit pas qu’elle tiendra. Ici, c’est le conteneur de sélection englobant qui prend la décision d’ajustement. fixedSize()

Créer l’exemple macOS complet

Créez une app SwiftUI pour macOS dans Xcode et définissez sa cible de déploiement sur macOS 13 ou une version ultérieure. Remplacez la déclaration d’app générée et la vue de contenu par le programme monofichier suivant. La cible doit contenir exactement une déclaration d’app marquée @main.

L’exemple utilise des identifiants explicites plutôt que le texte des étiquettes comme identité. La documentation d’Apple sur ForEach exige des éléments identifiés ou un chemin de clé d’identifiant explicite ; des identifiants distincts permettent aussi à deux éléments d’avoir le même titre affiché. ForEach

import SwiftUI

struct DemoItem: Identifiable {
    let id: Int
    let title: String
}

struct ItemChip: View {
    let title: String

    var body: some View {
        Text(title)
            .lineLimit(1)
            .padding(.horizontal, 10)
            .padding(.vertical, 6)
            .background(
                RoundedRectangle(cornerRadius: 8)
                    .fill(Color.blue.opacity(0.12))
            )
    }
}

struct PrefixCandidate: View {
    let items: [DemoItem]
    let visibleCount: Int

    var body: some View {
        HStack(spacing: 8) {
            ForEach(items.prefix(visibleCount)) { item in
                ItemChip(title: item.title)
            }

            if visibleCount < items.count {
                Text("\(items.count - visibleCount) more")
                    .lineLimit(1)
                    .padding(.horizontal, 10)
                    .padding(.vertical, 6)
                    .background(
                        RoundedRectangle(cornerRadius: 8)
                            .fill(Color.gray.opacity(0.15))
                    )
            }
        }
        .fixedSize()
    }
}

struct WholeItemRow: View {
    let items: [DemoItem]

    var body: some View {
        if items.isEmpty {
            Text("No items")
                .fixedSize()
        } else {
            ViewThatFits(in: .horizontal) {
                ForEach(
                    Array((0...items.count).reversed()),
                    id: \.self
                ) { visibleCount in
                    PrefixCandidate(
                        items: items,
                        visibleCount: visibleCount
                    )
                }
            }
        }
    }
}

struct ContentView: View {
    let items: [DemoItem] = [
        DemoItem(id: 1, title: "Sketch"),
        DemoItem(id: 2, title: "Color samples"),
        DemoItem(id: 3, title: "Shapes"),
        DemoItem(id: 4, title: "Paper textures"),
        DemoItem(id: 5, title: "Layout notes"),
        DemoItem(id: 6, title: "Preview")
    ]

    var body: some View {
        VStack(alignment: .leading, spacing: 16) {
            Text("Sample labels")
                .font(.headline)

            WholeItemRow(items: items)
                .frame(maxWidth: .infinity, alignment: .leading)

            Text("Resize the window to change how many labels fit.")
                .foregroundStyle(.secondary)
        }
        .font(.system(size: 14))
        .padding(24)
        .frame(minWidth: 320, idealWidth: 680, minHeight: 160)
    }
}

@main
struct WholeItemsDemoApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

Les identifiants des variantes correspondent aux nombres d’éléments visibles possibles, classés par ordre décroissant, du nombre total d’éléments jusqu’à zéro. Dans chaque variante, les identifiants des éléments identifient les étiquettes. Chaque itération crée un PrefixCandidate, qui contient une rangée horizontale.

Le cadre flexible externe permet à la rangée de recevoir l’espace horizontal disponible, tout en alignant le contenu sélectionné sur le bord de départ. Gardez fixedSize() sur la variante candidate à l’intérieur de ViewThatFits. L’appliquer à l’ensemble du conteneur de sélection modifierait la façon dont ce conteneur réagit à la largeur proposée par son parent.

Cet exemple de code source est complet, mais il n’a pas été compilé ni exécuté ici, car aucun outil Swift n’était disponible. Les calculs illustratifs de la section suivante ont été vérifiés séparément ; les largeurs réelles du texte doivent être vérifiées dans l’app Mac.

Inclure le badge dans le calcul de l’ajustement

La règle de décision utile est la suivante : sélectionner le plus grand nombre d’éléments visibles dont la variante complète tient. Le nombre d’éléments masqués correspond toujours au nombre total d’éléments moins le nombre d’éléments visibles dans cette variante. Aucun calcul distinct ne nécessite de synchronisation.

Considérez une rangée illustrative de quatre éléments, d’une largeur de 72, 96, 64 et 88 points, marges des éléments comprises. Supposons un espacement de 8 points et un badge de 60 points pour tout nombre non nul d’éléments masqués. Il s’agit de valeurs de calcul fictives, et non de mesures de l’exemple d’app.

Lorsque les quatre éléments sont visibles, la largeur requise est de 72 + 96 + 64 + 88 + 3 × 8 = 344 points. Avec trois éléments visibles, le badge remplace le suffixe omis et la largeur passe à 72 + 96 + 64 + 3 × 8 + 60 = 316 points. Trois espaces sont nécessaires, car la variante contient trois éléments et un badge.

Largeur disponible Éléments visibles Éléments masqués Largeur de la variante sélectionnée
344 points 4 0 344 points
343 points 3 1 316 points
300 points 2 2 244 points
220 points 1 3 140 points
60 points 0 4 60 points

Dans le programme, chaque variante contient le texte réel de son badge. Elle tient donc compte des différences entre les chaînes de badge, sans dépendre de l’hypothèse de largeur constante utilisée dans l’illustration. Les largeurs des variantes ne doivent pas nécessairement varier de manière uniforme ; la diminution du nombre d’éléments visibles exprime la préférence de sélection.

La variante contenant uniquement le décompte fixe la largeur minimale utilisable. Si même ce badge ne tient pas, il est impossible d’afficher le décompte complet dans l’espace disponible avec ces règles. Prévoyez une largeur minimale suffisante, autorisez une autre disposition ou déplacez le décompte ailleurs. Ne considérez pas le rognage comme un résultat satisfaisant pour des éléments entiers.

L’exemple définit une largeur minimale de contenu et une police explicite de taille modeste pour son badge synthétique court. Si vous modifiez le libellé, la police, les marges ou la mise en page environnante, vérifiez à nouveau le cas où le badge contient uniquement le décompte. La largeur minimale choisie pour cet exemple ne constitue pas une mesure universelle du badge.

Séparer la sélection de la mise en page du cycle de vie des vues

Le nombre d’éléments masqués fait partie des entrées immuables de chaque variante. Le programme ne modifie pas de liaison à partir d’une méthode de mesure, ne planifie pas de mise à jour asynchrone du décompte et ne déduit pas la visibilité de rappels d’apparition. Le choix de la rangée sélectionne aussi le badge correspondant.

Gardez le contenu répété de chaque variante simple. Répéter une étiquette dans plusieurs variantes ne crée pas une identité partagée pour cette étiquette dans toutes ces variantes. Apple explique que le stockage d’état est associé à l’identité de la vue et qu’un changement d’identité peut remplacer l’état. Demystify SwiftUI

Si vous remplacez ensuite les étiquettes par des contrôles modifiables, conservez leurs valeurs partagées dans un ancêtre commun ou un modèle, puis transmettez les entrées ou liaisons appropriées à chaque présentation. Apple recommande de placer l’état partagé dans le plus proche ancêtre commun des vues qui en ont besoin. Ne comptez pas sur la persistance de l’état local d’une variante lors d’un changement de présentation. Managing user interface state

Cette solution affiche le nombre d’éléments masqués dans la rangée sélectionnée. Elle ne transmet pas ce nombre à une vue sœur ni au modèle de l’application. L’initialiseur ViewThatFits documenté par Apple accepte des axes contraints et le contenu candidat ; il ne fournit aucune liaison au nombre sélectionné. Considérez l’exportation de cette valeur comme une exigence de conception supplémentaire. init(in:content:)

Savoir quand une mise en page personnalisée requiert une autre approche

Une Layout personnalisée convient lorsque vous avez besoin d’une géométrie particulière. Apple définit ses responsabilités principales comme le calcul de la taille composite et le positionnement des sous-vues au moyen de proxys de mise en page. Ces proxys exposent des opérations de mesure et de placement plutôt qu’un accès direct aux vues sous-jacentes. Layout

Ne mettez pas en œuvre l’omission en arrêtant simplement le placement après le dernier élément qui tient. La documentation d’Apple sur LayoutSubview indique que chaque sous-vue doit être placée une fois. Omettre le placement n’est pas une méthode documentée pour masquer une vue, et un proxy de mise en page ne fournit pas d’opération générale de réécriture du contenu permettant de transformer une autre sous-vue en nouvelle étiquette de décompte. LayoutSubview

Si vous créez une mise en page personnalisée, veillez à ce que le calcul de la taille et le placement soient cohérents, notamment pour l’espacement et la proposition de taille utilisée. Apple exige explicitement la cohérence de ces calculs. Cette exigence s’applique aussi lorsque la mise en page évalue plusieurs dispositions possibles. placeSubviews(in:proposal:subviews:cache:)

L’approche par variantes est volontairement limitée. Pour six éléments, elle décrit sept rangées candidates et 21 apparitions d’éléments dans ces rangées. De manière générale, le nombre d’apparitions d’éléments croît comme n × (n + 1) / 2. Il s’agit d’un décompte structurel, pas d’un test de performances ni d’une affirmation selon laquelle chaque variante est affichée à chaque mise à jour. Utilisez cette approche pour une petite collection d’étiquettes ; évaluez les performances avant de l’étendre à une grande collection ou à un contenu coûteux.

Vérifier les limites de la rangée

Lancez l’app et redimensionnez lentement sa fenêtre. Vérifiez les conditions suivantes avant d’adapter le composant :

  • Lorsque la fenêtre est large, toutes les étiquettes sont visibles et aucun badge 0 de plus n’apparaît.
  • À mesure que la fenêtre rétrécit, les étiquettes disparaissent à partir de la fin, sans étiquette partielle ni points de suspension.
  • Le nombre d’éléments visibles ajouté au nombre d’éléments masqués du badge est toujours égal à six.
  • Si la première étiquette est exceptionnellement longue, la variante contenant uniquement le décompte reste disponible.
  • Avec un tableau vide, la rangée affiche Aucun élément.
  • Avec des titres en double et des identifiants distincts, les deux éléments sont pris en compte dans le décompte.
  • Après avoir modifié les polices ou le libellé du badge, la variante contenant uniquement le décompte tient toujours dans la largeur minimale prise en charge.

Pour une petite rangée ordonnée, les variantes complètes rendent la règle d’ajustement facile à examiner : chaque variante inclut ses étiquettes, son espacement et le décompte exact des éléments masqués. Définissez explicitement la largeur minimale et gardez le contenu répété léger ; la rangée pourra alors s’adapter au redimensionnement de la fenêtre sans éléments partiels ni cycle distinct de mise à jour du décompte.

Sources