Pour corriger le contenu SwiftUI qui sort de l’écran lors du redimensionnement d’une fenêtre Mac, déterminez d’abord si le dessin dépasse les limites de sa zone de mise en page, si le contenu refuse de rétrécir ou si une vue AppKit intégrée cesse de suivre son conteneur. Réparez ensuite cette limite précise.

Ce guide s’adresse aux développeurs qui corrigent l’interface redimensionnable de leur propre app Mac : garder les libellés lisibles, les commandes accessibles et les contenus plus longs défilables. La séquence de diagnostic ci-dessous utilise des libellés fictifs et distingue la mise en page SwiftUI, le dimensionnement de la fenêtre et le retour à la ligne du texte AppKit.

Déterminer quelle limite pose problème pendant le redimensionnement

La mise en page SwiftUI commence par une proposition de taille envoyée par un parent. Un enfant choisit sa taille en fonction de cette proposition ; celle-ci ne lui ordonne pas de tout découper à ce rectangle. Apple documente les propositions de taille minimale, maximale et idéale dans ProposedViewSize. Cela explique pourquoi rétrécir une fenêtre ne réduit pas nécessairement la largeur de tous ses descendants.

Commencez par un test reproductible : ouvrez la fenêtre de votre app, rétrécissez-la jusqu’à l’apparition du premier problème, puis agrandissez-la de nouveau. Observez le premier élément affecté plutôt que la disposition finale. Un libellé dépasse-t-il de son conteneur ? Le panneau entier se déplace-t-il ? Le texte conserve-t-il sa largeur de retour à la ligne précédente ?

Ajoutez temporairement .border(Color.orange) à l’enfant suspect et .border(Color.blue) à son cadre ou conteneur parent. Placez les modificateurs à différents niveaux pour comparer leurs rectangles. La documentation d’Apple sur offset illustre cette technique de bordure pour distinguer la position initiale dans la mise en page du dessin déplacé.

Ce que vous observez À examiner d’abord Action utile ensuite
Le dessin sort d’un rectangle par ailleurs stable offset, position, calculs de coordonnées Retirer individuellement les modificateurs de positionnement
Un libellé reste large alors que son conteneur rétrécit fixedSize horizontal, largeurs explicites Permettre au libellé d’accepter une largeur contrainte
Les commandes ne tiennent pas sur une ligne horizontale Largeur cumulée des commandes et espacement Proposer une disposition verticale
Le contenu inférieur disparaît quand la hauteur diminue Absence de gestion du dépassement Rendre la zone de contenu défilable
Un panneau SwiftUI hébergé se déplace dans AppKit Limites de la vue hôte et contraintes de dimensionnement Comparer la taille de l’hôte aux besoins du contenu
Une vue texte AppKit rétrécit, mais les lignes restent larges Suivi de la largeur du conteneur de texte Vérifier la configuration de la vue texte et de son conteneur

Ce sont des hypothèses de diagnostic déduites des comportements documentés, et non la garantie qu’un symptôme particulier ait une cause unique. Modifiez un seul élément suspect à la fois et répétez le même redimensionnement. Si remplacer un enfant complexe par Text("Sample panel") élimine le problème, rétablissez progressivement les différentes couches de cet enfant.

Supprimer les règles de positionnement et de dimensionnement qui résistent à la fenêtre

Vérifier les décalages et le positionnement centré

Un offset déplace le contenu affiché sans modifier les dimensions initiales de la vue. Un décalage horizontal positif peut donc pousser le dessin au-delà du bord droit, même si le parent réserve toujours l’espace initial. Retirez temporairement le décalage ; si le dessin revient dans le conteneur, remplacez cet ajustement par une disposition exprimée à l’aide de piles, de marges internes ou de l’alignement du cadre. Voir le comportement de offset documenté par Apple.

position(x:y:) place le centre de la vue aux coordonnées indiquées dans l’espace de coordonnées de son parent. Considérer ces coordonnées comme le coin supérieur gauche introduit une erreur liée à la taille de l’enfant. Une position calculée une seule fois peut également ne plus convenir lorsque la taille du parent change. Pour un panneau courant composé de libellés et de commandes, préférez un alignement qui exprime la relation voulue avec le conteneur. Apple définit ce comportement centré dans position(x:y:).

Par exemple, .frame(maxWidth: .infinity, alignment: .leading) exprime un alignement au bord de tête dans un cadre flexible. Cela ne supprime pas les exigences de dimensionnement propres à un descendant trop grand. La documentation d’Apple sur les cadres flexibles explique comment le cadre contraint les propositions et positionne son enfant.

Vérifier les largeurs fixes et la taille idéale du texte

Examinez les réglages de largeur explicites et les utilisations horizontales de fixedSize dans la branche qui pose problème. Apple indique précisément que .fixedSize(horizontal: true, vertical: false) oblige le texte à conserver la largeur idéale nécessaire à la chaîne entière, qui peut dépasser les limites de son parent. Pour le texte courant avec retour à la ligne, supprimez cette contrainte horizontale. Lorsqu’il est important de préserver toute la hauteur du texte ainsi renvoyé à la ligne, envisagez .fixedSize(horizontal: false, vertical: true). Voir fixedSize(horizontal:vertical:).

Préserver la taille verticale suppose qu’un espace puisse accueillir cette hauteur. Un paragraphe qui grandit verticalement dans un conteneur court sans défilement peut rester inaccessible. Utilisez une zone de défilement vertical lorsque l’utilisateur doit pouvoir accéder à tout le texte ; Apple documente les axes de défilement pris en charge dans ScrollView.

Réexaminer les calculs fondés sur la géométrie

GeometryReader définit le contenu à l’aide de sa propre taille et de son espace de coordonnées, et propose à son parent une taille préférée flexible. L’ajouter autour d’une vue modifie la structure de mise en page ; ce n’est donc pas une simple enveloppe de mesure neutre. Si le décalage a commencé après l’ajout de calculs géométriques, remplacez temporairement cette branche par des piles et des cadres ordinaires. Apple décrit ces propriétés dans GeometryReader.

Pour une disposition proportionnelle qui nécessite réellement la géométrie, calculez les dimensions à partir du conteneur actuel pertinent. Vérifiez tout état qui stocke une largeur relevée lors de l’apparition initiale. Vérifiez également si la dimension mesurée sert à redimensionner la région même qui est mesurée ; supprimer cette dépendance est un bon test d’isolation.

Créer une disposition compacte avec un comportement de dépassement explicite

L’exemple complet ci-dessous illustre trois choix indépendants : le texte revient à la ligne, les commandes changent de disposition si nécessaire et le contenu qui dépasse verticalement reste accessible. Les dimensions sont des valeurs de conception illustratives, et non des valeurs prescrites par Apple.

Créez dans Xcode une app SwiftUI pour macOS, définissez sa cible de déploiement sur macOS 13 ou une version ultérieure, puis remplacez les déclarations générées de l’app et de la vue de contenu par ce fichier unique. Ne conservez qu’une seule déclaration @main. L’exemple utilise ViewThatFits et les API de redimensionnement de fenêtre disponibles à partir de macOS 13. Apple documente leur comportement dans ViewThatFits et windowResizability(_:).

import SwiftUI

@main
struct ResizeExampleApp: App {
    var body: some Scene {
        WindowGroup {
            ResizeExampleView()
                .frame(minWidth: 280, minHeight: 220)
        }
        .windowResizability(.contentMinSize)
    }
}

struct ResizeExampleView: View {
    @State private var selection = "Sample A"

    private let details = Array(
        repeating: "Sample notes wrap as the panel becomes narrower.",
        count: 12
    ).joined(separator: " ")

    var body: some View {
        ScrollView(.vertical) {
            VStack(alignment: .leading, spacing: 16) {
                Text("Sample panel")
                    .font(.title2)

                Text(details)
                    .fixedSize(horizontal: false, vertical: true)
                    .frame(maxWidth: .infinity, alignment: .leading)

                ViewThatFits(in: .horizontal) {
                    HStack(spacing: 12) {
                        Button("Choose Sample A") {
                            selection = "Sample A"
                        }
                        Button("Choose Sample B") {
                            selection = "Sample B"
                        }
                    }
                    .fixedSize(horizontal: true, vertical: false)

                    VStack(alignment: .leading, spacing: 12) {
                        Button("Choose Sample A") {
                            selection = "Sample A"
                        }
                        Button("Choose Sample B") {
                            selection = "Sample B"
                        }
                    }
                }

                Text("Selected: \(selection)")
            }
            .padding(20)
            .frame(
                minWidth: 0,
                maxWidth: .infinity,
                alignment: .leading
            )
        }
    }
}

ViewThatFits évalue les options dans l’ordre déclaré et choisit la première dont la taille idéale convient aux axes contraints. Ici, l’ajustement horizontal permet de choisir entre la rangée de boutons et la pile verticale. La rangée préserve volontairement sa taille idéale horizontale pour qu’une rangée compressée ne devienne pas le résultat privilégié. Veillez à ce que les deux options proposent les mêmes actions. Voir les règles de sélection d’Apple.

Le cadre extérieur permet à la colonne de contenu de suivre la largeur disponible, tandis que la vue de défilement vertical donne accès au contenu lorsque le retour à la ligne augmente sa hauteur. Cette disposition suppose que chaque commande tient dans la largeur minimale choisie. Des libellés plus longs ou des polices plus grandes peuvent nécessiter une autre largeur minimale ou une disposition compacte différente.

La cohérence des déclarations et la disponibilité des API de cet exemple ont été vérifiées ; il n’a pas été compilé ici, car aucun compilateur Swift n’était disponible. Exécutez-le sous macOS pour vérifier le comportement réel de la fenêtre et du texte.

Définir les limites de la fenêtre après avoir corrigé la mise en page du contenu

Une taille minimale de fenêtre adaptée évite que l’interface devienne plus petite que la disposition qu’elle prend en charge. Avec .contentMinSize, SwiftUI déduit la taille minimale de la fenêtre de son contenu et n’impose aucune taille maximale. Dans l’exemple, le cadre racine fournit les valeurs minimales de largeur et de hauteur. Voir contentMinSize.

Choisissez cette taille minimale en testant la disposition compacte avec les libellés pris en charge les plus longs. Augmenter la taille minimale jusqu’à ce qu’une vue mal positionnée devienne visible peut masquer l’erreur de mise en page initiale. Supprimez d’abord les décalages involontaires et les enfants trop grands ; définissez ensuite les dimensions minimales qui permettent encore d’effectuer la tâche prévue.

Si AppKit gère la fenêtre et héberge SwiftUI, examinez séparément cette frontière d’hébergement. À partir de macOS 13, NSHostingView.sizingOptions contrôle les contraintes dérivées du contenu SwiftUI. Lorsque la vue hôte est la contentView de la fenêtre, elle met également à jour les tailles minimale et maximale du contenu de la fenêtre. Apple précise aussi que le contenu est centré lorsque le cadre de l’hôte diffère de la taille requise par son contenu SwiftUI. Vérifiez ces relations avant d’ajouter des décalages compensatoires. Voir les options de dimensionnement de NSHostingView.

Vérifier AppKit et TextKit uniquement si la vue concernée les utilise

Laisser SwiftUI dimensionner la vue racine représentée

Pour une NSViewRepresentable, SwiftUI contrôle la frame et la bounds de la vue AppKit représentée. Apple avertit explicitement que définir ces propriétés vous-même entre en conflit avec SwiftUI et entraîne un comportement indéfini. Si votre gestionnaire de redimensionnement réattribue sans cesse le cadre de la vue racine représentée, supprimez cette gestion concurrente et exprimez le dimensionnement à la frontière SwiftUI. Voir NSViewRepresentable.

Lorsqu’une mesure personnalisée est nécessaire, macOS 13 et les versions ultérieures proposent sizeThatFits(_:nsView:context:). Cette méthode peut recevoir plusieurs propositions au cours d’une même passe de mise en page ; renvoyer nil demande le dimensionnement par défaut. Faites dépendre le résultat de la proposition actuelle et du contenu, et non de la largeur précédente de la fenêtre. Une dimension proposée non spécifiée nécessite une règle de mesure réfléchie ; elle ne doit pas être automatiquement considérée comme égale à zéro. Voir le dimensionnement des vues représentées.

La liaison optionnelle de Swift permet de vérifier la présence d’une dimension proposée et de l’extraire avant de l’utiliser. C’est utile pour mettre en œuvre une règle de dimensionnement sans déballer de force les largeurs ou hauteurs optionnelles. Le langage documente ce comportement dans Les bases : liaison optionnelle.

Distinguer la largeur de retour à la ligne de celle de la vue texte

Si l’enfant concerné est une NSTextView, comparez sa largeur actuelle à celle de son conteneur de texte. Apple indique que NSTextContainer.widthTracksTextView vaut false par défaut ; le définir sur true fait ajuster la largeur du conteneur lorsque celle de la vue texte change. C’est le réglage pertinent si les lignes doivent se reformer en fonction de la largeur de la vue texte. Voir widthTracksTextView.

Examinez également isHorizontallyResizable. Cette propriété détermine si la vue texte change de largeur pour s’adapter à son texte ; cette exigence diffère du retour à la ligne dans une largeur attribuée. Pour une zone de texte à retour à la ligne qui défile verticalement, configurez ces deux règles de façon cohérente et vérifiez que la mise en page contenante redimensionne bien la vue texte. Voir isHorizontallyResizable.

Conservez le moteur de texte utilisé par votre implémentation. Apple précise que l’accès à layoutManager sur une NSTextView ou son conteneur de texte peut faire basculer la vue en mode de compatibilité utilisant NSLayoutManager. N’ajoutez pas cet accès sans raison lorsque vous diagnostiquez une vue TextKit 2. Utilisez les API adaptées au moteur existant et à la cible de déploiement. Voir NSTextView.

Vérifier la correction après des changements de largeur et de hauteur

Suivez cette séquence finale dans votre app :

  1. Rétrécissez lentement la fenêtre jusqu’au point où le problème apparaissait, puis agrandissez-la de nouveau.
  2. Réduisez la hauteur indépendamment et vérifiez que tout le contenu nécessaire reste accessible.
  3. Recommencez avec de longs libellés fictifs et du texte sur plusieurs lignes.
  4. Activez chaque commande dans les dispositions large et compacte.
  5. Pour une vue texte AppKit, vérifiez que le retour à la ligne suit la largeur actuelle dans les deux sens du redimensionnement.

Une correction réussie maintient le positionnement lié au conteneur actuel, propose une disposition compacte utilisable et permet d’accéder au contenu qui dépasse. Lorsque ces comportements sont confirmés après plusieurs redimensionnements, retirez les bordures temporaires et conservez la taille minimale de fenêtre justifiée par l’interface.

Sources