Pour que les colonnes du libellé et de l’état d’une fenêtre Mac restent stables tandis que sa description s’agrandit, donnez aux colonnes extérieures des largeurs explicites et attribuez un cadre flexible à la description. Utilisez la priorité de mise en page lorsque des vues sœurs sont en concurrence, et préservez la taille idéale uniquement sur les axes qui en ont besoin.

Ce tutoriel crée une petite fenêtre redimensionnable contenant des libellés et des descriptions d’éléments fictifs. L’objectif est de garder les colonnes alignées tout en permettant aux descriptions plus longues de revenir à la ligne lorsque la fenêtre rétrécit. Tout appartient à l’app d’exemple ; la mise en page utilise des vues SwiftUI ordinaires.

Quel contrôle de dimensionnement chaque sous-vue doit-elle utiliser ?

La mise en page SwiftUI commence par une proposition de taille du parent. Chaque enfant répond avec la taille qu’il préfère, puis le conteneur organise les résultats. Pour commencer, il est utile de décider quelles dimensions chaque enfant peut modifier. Apple explique ce processus de proposition et de réponse dans Un guide de la mise en page dans SwiftUI.

Pour cette fenêtre, attribuez trois rôles : une colonne de libellés d’une largeur choisie, une colonne de descriptions qui absorbe les changements de largeur, et une colonne d’état d’une largeur choisie. Distinguez cette largeur fixe numérique de la taille idéale d’une vue : elles expriment des exigences différentes.

Exigence Contrôle Conséquence pratique
Réserver une largeur de colonne précise .frame(width: 96) Le cadre obtenu a cette largeur.
Permettre à une description d’absorber la largeur disponible .frame(minWidth: 160, maxWidth: .infinity, alignment: .leading) Le cadre accepte la largeur proposée, sous réserve de sa valeur minimale.
Préserver la taille idéale du contenu sur certains axes .fixedSize(horizontal: false, vertical: true) La largeur peut s’adapter à la mise en page, tandis que la hauteur conserve sa taille idéale.
Favoriser une vue sœur lors de la répartition de l’espace .layoutPriority(1) Le parent privilégie cet enfant par rapport aux vues sœurs de priorité inférieure.

Apple documente les dimensions numériques dans le modificateur de cadre fixe, les contraintes minimales et maximales dans le modificateur de cadre flexible, le dimensionnement idéal par axe dans fixedSize(horizontal:vertical:), et la préférence entre vues sœurs dans layoutPriority(_:).

Une valeur maximale infinie autorise l’agrandissement dans les limites d’une proposition ; elle ne demande pas une fenêtre d’une largeur infinie. En fournissant des contraintes minimales et maximales, le cadre adopte la dimension proposée, limitée par ces contraintes. Le paramètre d’alignement positionne l’enfant à l’intérieur de ce cadre. Ces détails comptent lorsque le texte lui-même est moins large que l’espace attribué à sa colonne. Documentation d’Apple sur les cadres flexibles

Créer un exemple macOS complet et redimensionnable

Dans Xcode, créez un projet d’app macOS utilisant SwiftUI et définissez sa cible de déploiement sur macOS 13 ou une version ultérieure. Remplacez les déclarations générées de l’app et de la vue de contenu par ce fichier unique, en ne conservant qu’un seul point d’entrée d’app. L’exemple utilise le contrôle de redimensionnement des fenêtres de macOS 13. Documentation d’Apple sur WindowResizability

Voici un exemple d’app complet. Il comprend les chaînes de démonstration et une option permettant de passer de descriptions courtes à longues. L’app SwiftUI n’a pas pu être compilée dans l’environnement de recherche, faute de chaîne d’outils Swift ; vérifiez son comportement dans Xcode avant de l’intégrer à un projet existant.

import SwiftUI

@main
struct ColumnAllocationApp: App {
    var body: some Scene {
        WindowGroup {
            AllocationView()
                .frame(minWidth: 420, minHeight: 260)
        }
        .windowResizability(.contentMinSize)
    }
}

struct AllocationView: View {
    @State private var showLongDescriptions = true

    var body: some View {
        VStack(alignment: .leading, spacing: 16) {
            Text("Item overview")
                .font(.title2)

            Toggle(
                "Show longer descriptions",
                isOn: $showLongDescriptions
            )

            ScrollView {
                VStack(alignment: .leading, spacing: 16) {
                    AllocationRow(
                        label: "Item A",
                        summary: showLongDescriptions
                            ? "A blue folder with several sample notes and a longer descriptive label."
                            : "Blue folder",
                        status: "Ready"
                    )

                    AllocationRow(
                        label: "Item B",
                        summary: showLongDescriptions
                            ? "A green notebook containing a short list of synthetic example entries."
                            : "Green notebook",
                        status: "Draft"
                    )
                }
                .frame(maxWidth: .infinity, alignment: .leading)
            }
        }
        .padding(16)
    }
}

struct AllocationRow: View {
    let label: String
    let summary: String
    let status: String

    var body: some View {
        HStack(alignment: .top, spacing: 12) {
            Text(label)
                .frame(width: 96, alignment: .leading)

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

            Text(status)
                .fixedSize(horizontal: true, vertical: false)
                .frame(width: 72, alignment: .trailing)
        }
    }
}

La description est la seule colonne dont la largeur est flexible. Son appel à fixedSize laisse le dimensionnement horizontal s’adapter, tout en préservant l’espace vertical requis par le texte renvoyé à la ligne. Son cadre flexible remplit ensuite la colonne attribuée. L’état préserve la largeur idéale de son texte à l’intérieur d’un cadre distinct de 72 points. Apple avertit qu’un contenu de taille idéale peut dépasser les limites de son parent ; ce traitement de l’état suppose donc que les valeurs fournies, Ready et Draft, restent courtes. Documentation d’Apple sur fixedSize

La hauteur des lignes n’est pas fixe. C’est intentionnel : les descriptions plus étroites peuvent nécessiter davantage de lignes. La vue de défilement permet d’accéder aux lignes lorsque leur hauteur cumulée dépasse l’espace disponible. Évitez d’ajouter une hauteur de ligne restrictive uniquement pour rendre la disposition initiale uniforme.

La fenêtre utilise .contentMinSize, qui déduit ses dimensions minimales du contenu et n’impose aucune taille maximale. Ici, la largeur minimale de 420 points de la vue racine établit une limite inférieure pratique pour l’exemple. Ce contrôle s’applique aux fenêtres créées par la scène de l’app. Documentation d’Apple sur contentMinSize

Les déclarations qui renvoient some View utilisent les types de résultat opaques de Swift : leurs types concrets restent masqués, tandis que leur conformité au protocole View reste disponible. Vous pouvez modifier les chaînes de modificateurs sans écrire les types de retour imbriqués. Le langage de programmation Swift : types de protocole opaques et encadrés

Calculer le budget de largeur avant de modifier les priorités

Un budget de largeur rend explicites les limites de la mise en page. Dans cet exemple, soustrayez les marges horizontales, les deux espacements et les deux colonnes fixes de la largeur du contenu de la fenêtre. Le reste revient à la description. Il s’agit de dimensions de conception illustratives, et non de mesures de contrôles natifs.

Largeur de la description
    = largeur du contenu - marges horizontales - espacements - colonnes fixes
    = largeur du contenu - (2 × 16) - (2 × 12) - 96 - 72
    = largeur du contenu - 224

À 640 points : il reste 416 points pour la description.
À 420 points : il reste 196 points pour la description.

Budget minimal de la ligne :
    32 + 24 + 96 + 160 + 72 = 384 points.

Le calcul a été vérifié au moyen d’un calcul local déterministe. Il décrit cette ligne lorsqu’elle reçoit la largeur du contenu diminuée des marges indiquées ; il exclut les décorations de la fenêtre. La largeur minimale choisie de 420 points laisse une marge par rapport au minimum calculé de 384 points pour la ligne. Si un conteneur environnant ajoute des marges internes, incluez-les également dans le budget.

Ce calcul révèle aussi une demande impossible. En dessous du budget minimal total, les trois colonnes ne peuvent pas toutes conserver simultanément leurs largeurs et leurs espacements. Choisissez une largeur minimale de fenêtre plus grande, des dimensions minimales plus petites ou une disposition différente. Augmenter la priorité ne peut pas créer d’espace supplémentaire.

Quand la priorité de mise en page est-elle utile ?

La priorité devient utile lorsque deux vues sœurs peuvent toutes deux rétrécir et que vous souhaitez que l’une d’elles conserve davantage d’espace. Par exemple, un titre descriptif peut être prioritaire sur une annotation secondaire. Placez .layoutPriority(1) sur l’enfant qui participe à la décision de répartition de l’espace de ce parent. Apple indique que les vues de priorité supérieure rétrécissent plus tard et s’étirent plus tôt, l’espace nécessaire aux exigences minimales des enfants de priorité inférieure étant réservé. Documentation d’Apple sur layoutPriority

L’exemple complet n’a pas besoin de modificateur de priorité : sa stratégie de répartition est déjà exprimée par deux colonnes fixes et une colonne flexible. Une valeur de priorité n’est pas non plus un poids proportionnel. Attribuer les priorités 2 et 1 ne définit pas un rapport de largeur de deux pour un ; le but documenté est d’exprimer une préférence lors de la répartition. Documentation d’Apple sur layoutPriority

Évitez d’ajouter un Spacer horizontal entre la description et l’état, sauf si vous souhaitez un espacement flexible distinct. Un spacer s’agrandit le long de l’axe principal de sa pile et écarte les vues sœurs. Comme le cadre de la description absorbe déjà les changements de largeur, cet élément supplémentaire est inutile ici. Documentation d’Apple sur Spacer

Que doit-il se passer lorsque le texte ne tient plus ?

Décidez séparément si chaque champ de texte doit revenir à la ligne, être tronqué ou nécessiter une disposition différente. Les descriptions de l’exemple peuvent revenir à la ligne sans limite. Appliquer .lineLimit(1) limiterait chaque élément de texte concerné à une seule ligne et modifierait ainsi l’exigence. Une limite de lignes de nil supprime cette restriction, mais ne supprime pas les contraintes imposées par les cadres environnants. Documentation d’Apple sur lineLimit

Pour des valeurs d’état plus longues, réévaluez la colonne de 72 points. Supprimez la préservation de la taille idéale horizontale si un retour à la ligne est acceptable, augmentez la largeur de la colonne ou placez l’état sous la description. Conserver .fixedSize(horizontal: true, vertical: false) tout en rétrécissant le cadre qui l’entoure peut permettre au texte de dépasser ce cadre. Documentation d’Apple sur fixedSize

Si une fenêtre compacte doit réorganiser le contenu, macOS 13 et les versions ultérieures proposent ViewThatFits. Fournissez d’abord une disposition horizontale, puis une disposition empilée. Le système sélectionne le premier enfant dont la taille idéale tient sur les axes contraints ; évaluez donc l’espace horizontal avec ViewThatFits(in: .horizontal) lorsque la largeur est le facteur déterminant. Assurez-vous que la dernière option reste utilisable à la largeur minimale prise en charge. Documentation d’Apple sur ViewThatFits

Considérez cela comme une autre stratégie de mise en page. L’exemple actuel conserve délibérément les colonnes et définit une largeur minimale de fenêtre. Utilisez une disposition empilée de remplacement si le maintien de la disposition horizontale compte moins que la possibilité de réduire davantage la fenêtre.

Comment les vues AppKit et TextKit intégrées modifient-elles l’approche ?

Si la description devient par la suite un composant de texte AppKit, utilisez NSViewRepresentable pour intégrer la vue. Apple précise que SwiftUI contrôle frame et bounds de la vue représentée ; modifier directement ces propriétés dans votre propre code entre en conflit avec la mise en page SwiftUI. Configurez le contenu au moyen du cycle de vie du représentable et exprimez le dimensionnement extérieur avec SwiftUI. Documentation d’Apple sur NSViewRepresentable

Depuis macOS 13, la méthode sizeThatFits(_:nsView:context:) du représentable peut renvoyer sa taille préférée pour une proposition donnée. Elle peut être appelée plusieurs fois avec différentes propositions au cours d’une même passe de mise en page. Renvoyer nil demande l’algorithme de dimensionnement par défaut. La mesure d’un composant avec retour à la ligne doit tenir compte de la largeur proposée, au lieu de toujours renvoyer la largeur du texte sans retour à la ligne. Documentation d’Apple sur le dimensionnement des représentables

Distinguez les dimensions non spécifiées de la valeur zéro : SwiftUI utilise une proposition non spécifiée pour demander la taille idéale, tandis que zéro peut servir à sonder la taille minimale. Une routine de mesure personnalisée doit traiter ces cas délibérément. Documentation d’Apple sur ProposedViewSize

Dans une mise en page AppKit, la résistance à la compression détermine à quel point une NSView résiste à une réduction en dessous de sa taille intrinsèque. Vérifiez ce réglage lorsqu’un composant avec retour à la ligne résiste à la compression, en plus de son comportement de dimensionnement SwiftUI. Documentation d’Apple sur setContentCompressionResistancePriority

Pour un NSTextView utilisant TextKit 2, utilisez la voie moderne textLayoutManager. La présentation actuelle de TextKit par Apple la distingue de l’ancienne layoutManager, et ses instructions de migration expliquent que l’accès à cette propriété ancienne peut provoquer un basculement de compatibilité irréversible vers TextKit 1 pour cette instance. Il n’est pas nécessaire de changer de moteur de texte pour répartir l’espace entre les colonnes sœurs. Présentation de TextKit par Apple, Nouveautés de TextKit et des vues de texte

Vérifier le résultat aux limites de la fenêtre

Exécutez l’exemple et vérifiez les points suivants avant d’appliquer cette méthode à une interface plus grande :

  1. Élargissez la fenêtre : la colonne de description doit s’agrandir, tandis que les cadres du libellé et de l’état conservent leur largeur.
  2. Réduisez-la jusqu’à la largeur minimale prise en charge : les descriptions longues doivent revenir à la ligne et augmenter la hauteur des lignes.
  3. Activez puis désactivez Show longer descriptions : les descriptions courtes doivent rester alignées sur les mêmes colonnes.
  4. Remplacez un état fictif par un état plus long : décidez s’il faut élargir ce champ, le faire revenir à la ligne ou le réorganiser.
  5. Réduisez la hauteur disponible : vérifiez que toutes les lignes restent accessibles.

Pour voir quelle couche reçoit l’espace, ajoutez temporairement des bordures juste avant et après le cadre flexible de la description. La bordure intérieure montre les limites du texte ; la bordure extérieure montre celles de la colonne qui lui est attribuée. Apple recommande les bordures colorées comme moyen pratique de diagnostiquer le dimensionnement des piles et les marges internes. Un guide de la mise en page dans SwiftUI

À mesure que l’interface s’agrandit, gardez la stratégie de dimensionnement explicite : réservez des largeurs là où l’alignement l’exige, attribuez une région flexible définie au contenu variable, et décidez du comportement de chaque champ à la largeur minimale. Avec cette stratégie et un budget de largeur, vous pouvez modifier les libellés ou ajouter du contenu sans devoir deviner quel modificateur devrait recevoir une priorité plus élevée.

Sources