Pour qu’une vue de défilement SwiftUI s’adapte à un contenu court, mesurez son contenu intérieur et définissez la hauteur de la zone d’affichage comme la plus petite de cette mesure et d’une limite choisie. Un contenu plus long défile alors dans la limite fixée. Ce guide met en place ce comportement pour un petit panneau d’étiquettes dans votre propre app Mac.

Pourquoi une vue de défilement courte laisse-t-elle un espace vide ?

Commencez par distinguer deux dimensions : la hauteur du contenu et celle de la zone d’affichage qui le montre. Apple décrit ScrollView comme affichant une partie de son contenu sous-jacent et ajustant la partie visible pendant le défilement. Un petit groupe d’étiquettes ne détermine pas, à lui seul, la hauteur de zone d’affichage souhaitée. La documentation d’Apple sur ScrollView explique cette distinction.

Pour un panneau compact sur ordinateur, définissez une règle de dimensionnement explicite : viewportHeight = min(contentHeight, maximumHeight). Mesurez le contenu à la largeur qu’il reçoit réellement, en incluant tout espacement intérieur qui doit défiler avec lui. Appliquez le résultat à la vue de défilement externe. min(_:_:) de Swift renvoie la plus petite valeur, tandis que frame(width:height:alignment:) de SwiftUI définit une hauteur fixe lorsque son argument de hauteur est fourni. Swift min(_:_:), Cadre fixe de SwiftUI.

À titre d’exemple, supposons que la limite maximale soit de 240 points. Un contenu de 86 points produit une zone d’affichage de 86 points ; un contenu de 410 points produit une zone d’affichage de 240 points, le reste du contenu étant accessible par défilement. Il s’agit de valeurs illustratives, et non de mesures tirées de l’exemple ci-dessous.

Modification Élément contrôlé Définit-elle la hauteur compacte souhaitée ?
Hauteur du contenu mesurée, plafonnée et appliquée à la vue de défilement Hauteur de la zone d’affichage Oui, dans la disposition contrainte présentée ci-dessous
.scrollBounceBehavior(.basedOnSize) Comportement du rebond selon la taille du contenu Non
.fixedSize(horizontal: false, vertical: true) sur les étiquettes Taille verticale idéale du contenu Cela facilite la mesure ; la zone d’affichage externe a toujours besoin d’une règle de hauteur

Apple décrit .basedOnSize comme une configuration du rebond et fixedSize(horizontal:vertical:) comme fixant une vue à sa taille idéale selon les dimensions choisies. Aucune de ces descriptions ne définit une hauteur de zone d’affichage dérivée du contenu. Comportement du rebond au défilement, Taille fixe par axe.

Créer un panneau compact avec une hauteur de défilement maximale

Utilisez une version actuelle de Xcode dont le SDK macOS expose la surcharge à valeur unique de onGeometryChange(for:of:action:). Apple indique actuellement que cette surcharge est disponible à partir de macOS 13.0. L’exemple complet ci-dessous cible macOS 13.3 ou une version ultérieure, car il utilise également scrollBounceBehavior(_:axes:), disponible à partir de macOS 13.3. Rappel de modification de la géométrie, Disponibilité du modificateur de rebond.

Créez un projet d’app macOS avec SwiftUI. Remplacez la déclaration d’app et la vue de contenu générées par ce programme en un seul fichier, en ne conservant qu’une seule déclaration @main dans la cible. Toutes les étiquettes sont des chaînes locales synthétiques. Le bouton Afficher plus d’étiquettes permet de basculer entre un contenu court et un contenu long.

import SwiftUI

@main
struct CompactLabelsApp: App {
    var body: some Scene {
        WindowGroup {
            CompactLabelsView()
        }
    }
}

struct CompactLabelsView: View {
    @State private var showMore = false
    @State private var viewportHeight: CGFloat = 240

    private var labels: [String] {
        if showMore {
            return (1...18).map { number in
                "Sample label \(number): a longer description that can wrap when the panel becomes narrower."
            }
        }
        return ["Sample label A", "Sample label B"]
    }

    var body: some View {
        VStack(alignment: .leading, spacing: 12) {
            Toggle("Show more labels", isOn: $showMore)

            ScrollView(.vertical) {
                VStack(alignment: .leading, spacing: 8) {
                    if labels.isEmpty {
                        Text("No labels to display")
                    } else {
                        ForEach(labels.indices, id: \.self) { index in
                            Text(labels[index])
                        }
                    }
                }
                .font(.body)
                .fixedSize(horizontal: false, vertical: true)
                .frame(maxWidth: .infinity, alignment: .leading)
                .padding(12)
                .onGeometryChange(for: CGFloat.self) { proxy in
                    min(proxy.size.height.rounded(.up), 240)
                } action: { newHeight in
                    viewportHeight = newHeight
                }
            }
            .frame(height: viewportHeight)
            .scrollBounceBehavior(.basedOnSize, axes: .vertical)
            .border(Color.secondary.opacity(0.4))

            Text("The label panel grows up to 240 points.")
                .font(.caption)
        }
        .padding(20)
        .frame(
            maxWidth: .infinity,
            maxHeight: .infinity,
            alignment: .topLeading
        )
        .frame(minWidth: 300, minHeight: 360)
    }
}

Le programme a été examiné, mais il n’a pas été compilé ni exécuté ici, car cet environnement ne dispose ni d’un compilateur Swift ni de l’environnement d’exécution macOS. Compilez-le dans Xcode et effectuez les vérifications ci-dessous avant de l’intégrer à une interface existante.

Pourquoi la mesure doit être effectuée à l’intérieur de la vue de défilement

Le rappel de géométrie observe la pile interne avec son espacement. Mesurer la vue de défilement externe renverrait la zone d’affichage que vous avez déjà définie, créant une règle de dimensionnement circulaire au lieu de déterminer la hauteur nécessaire au contenu.

La transformation ne renvoie que la hauteur arrondie et plafonnée. Apple recommande de transformer la géométrie en la valeur dont vous avez besoin et d’éviter les mises à jour trop générales lors de changements fréquents de géométrie. Ici, les changements de position de défilement n’ont aucune incidence sur le calcul, et un contenu dépassant la limite continue de produire le même résultat de 240 points. Recommandations d’Apple sur les changements de géométrie.

L’arrondi vers le haut évite d’attribuer volontairement une hauteur de zone d’affichage légèrement inférieure à celle mesurée pour le contenu. La valeur initiale de 240 points fournit une zone d’affichage utilisable avant l’arrivée de la mesure ; c’est une valeur d’amorçage, pas une hauteur de contenu mesurée. Le rappel la remplace ensuite.

Pourquoi la largeur et l’ordre des modificateurs sont importants

Gardez une largeur flexible. L’exemple utilise .fixedSize(horizontal: false, vertical: true) pour que les étiquettes puissent exploiter la largeur disponible tout en conservant leur taille verticale idéale. Apple avertit que fixer une dimension idéale peut faire dépasser à une vue les limites de son parent ; fixer les deux dimensions ne convient donc pas par défaut aux étiquettes qui passent à la ligne dans un panneau étroit. Documentation d’Apple sur fixedSize(horizontal:vertical:).

L’espacement intérieur de 12 points précède le modificateur de mesure, afin qu’il soit inclus dans la hauteur mesurée du contenu. La bordure vient après la définition de la hauteur externe et délimite la zone d’affichage. Cette disposition permet de voir plus facilement si l’espace inutilisé appartient au panneau défilant ou à la fenêtre qui l’entoure.

Vérifier le contenu court, le retour à la ligne et la réduction de taille

Suivez ces étapes pour vérifier le comportement de l’exemple, puis recommencez après avoir placé le panneau dans le conteneur réellement utilisé par votre app :

  1. Lancez l’app avec le bouton Afficher plus d’étiquettes désactivé. La bordure doit entourer les deux étiquettes et leur espacement intérieur, avec la légende juste en dessous.
  2. Activez Afficher plus d’étiquettes. Le panneau doit atteindre sa limite de 240 points. Faites défiler pour vérifier que la dernière étiquette reste accessible.
  3. Réduisez puis augmentez la largeur de la fenêtre. La hauteur des étiquettes renvoyées à la ligne doit changer sans que leurs dernières lignes disparaissent.
  4. Désactivez de nouveau le bouton. Le panneau doit retrouver la hauteur correspondant au contenu court.
  5. Remplacez temporairement le tableau court par []. Le message explicite Aucune étiquette à afficher doit déterminer la hauteur du contenu.
  6. Recommencez avec les styles de texte et les commandes environnantes utilisés dans votre vrai panneau.

L’état vide relève d’un choix délibéré d’interface : dans cet exemple, un message et l’espacement intérieur sont conservés lorsque le tableau est vide. Si vous souhaitez plutôt supprimer entièrement le panneau, omettez la vue de défilement entière lorsqu’il n’y a aucune étiquette. Cette décision de produit est distincte de la mesure d’un panneau non vide.

La fenêtre de l’exemple conserve sa hauteur minimale. Le code dimensionne la zone d’affichage des étiquettes ; il ne demande pas à la fenêtre Mac de rétrécir lorsque des étiquettes sont supprimées. Tout espace sous la légende appartient à la disposition externe, hors de la vue de défilement bordée.

Diagnostiquer les espaces restants ou les étiquettes coupées

Si le panneau reste trop haut, vérifiez d’abord où le modificateur de mesure est attaché. Il doit observer la pile de contenu après son espacement intérieur. Examinez ensuite les vues parentes pour repérer une hauteur ou une hauteur minimale explicite qui réserve de l’espace autour du panneau. Apple décrit la hauteur fournie à un cadre comme une dimension fixe ; un cadre externe distinct peut donc conserver de l’espace même si la zone d’affichage intérieure rétrécit. Dimensionnement avec les cadres SwiftUI.

Si le contenu lui-même reste anormalement haut, supprimez toute expansion verticale du sous-arbre mesuré. Pour ce modèle de petit groupe d’étiquettes, évitez un espaceur vertical ou un cadre de contenu dont la hauteur minimale dépend de celle de la zone d’affichage. Ces choix font dépendre la hauteur du contenu de celle de la zone d’affichage et compromettent ainsi la mesure indépendante nécessaire à la règle de dimensionnement.

Si l’espacement inférieur est coupé, vérifiez qu’il est inclus avant la mesure. Si le texte se répartit différemment après l’intégration, examinez la largeur réellement offerte par le nouveau parent. Une hauteur mesurée pour une largeur ne doit pas être considérée comme une hauteur permanente pour une autre largeur.

Choisissez une limite de 240 points adaptée à l’interface qui contient le panneau. L’exemple prévoit une hauteur minimale de fenêtre suffisante pour ses commandes, son espacement intérieur, sa légende et son panneau plafonné. Un conteneur plus court nécessite un budget plus petit ou une autre disposition environnante ; cette limite fixe ne calcule pas automatiquement l’espace disponible sous les autres commandes.

Utilisez cette approche pour un nombre modeste d’étiquettes disposées immédiatement. Apple décrit LazyVStack comme créant les éléments uniquement lorsque le rendu à l’écran l’exige. Ne supposez donc pas que remplacer la pile interne par une pile différée conserve la même stratégie de mesure de l’ensemble du contenu. Pour une grande collection, une zone d’affichage de taille fixe avec un contenu différé est généralement plus adaptée. Documentation d’Apple sur LazyVStack.

Quand les mesures AppKit et TextKit s’appliquent-elles ?

Pour ce panneau d’étiquettes SwiftUI, mesurez la sous-arborescence SwiftUI affichée. Une mesure de texte AppKit introduirait une autre configuration de mise en page, qui devrait correspondre aux étiquettes affichées.

Si votre app utilise déjà un NSScrollView, conservez la même distinction entre le document et la zone d’affichage. Apple définit documentView comme la vue que l’on fait défiler, tandis que contentSize désigne la taille de la vue de contenu de la vue de défilement. Malgré son nom, contentSize ne mesure pas l’ensemble du document. AppKit documentView, AppKit contentSize.

Pour une disposition TextKit existante construite autour de NSLayoutManager, usedRect(for:) renvoie le rectangle englobant des glyphes déjà disposés dans un conteneur de texte ; Apple précise explicitement que son interrogation ne génère pas de glyphes et n’effectue pas de mise en page. Lorsqu’une mise en page complète du conteneur est nécessaire, ensureLayout(for:) est la méthode documentée pour la forcer au préalable. TextKit usedRect(for:), TextKit ensureLayout(for:).

Même dans ce cas, le rectangle des glyphes ne correspond pas au cadre complet de la vue de texte. Apple documente l’espace supplémentaire autour du conteneur de texte via textContainerInset, ainsi que l’espacement des fragments de ligne à l’intérieur de celui-ci. Tenez compte de la configuration réelle de la vue de texte au lieu de copier directement la hauteur des glyphes dans une zone de défilement. Ces méthodes décrivent la voie NSLayoutManager ; elles ne constituent pas une méthode universelle de dimensionnement pour tous les composants de texte. AppKit textContainerInset.

Pour un petit panneau d’étiquettes SwiftUI, la solution pratique consiste à garder une largeur flexible, à mesurer le contenu intérieur avec son espacement et à plafonner la hauteur de la zone d’affichage externe. Vérifiez le résultat avec des contenus courts et longs, puis testez-le dans la véritable disposition parente afin que les contraintes environnantes permettent le même comportement compact.

Sources