Pour faire correspondre les commandes latérales à un aperçu SwiftUI avec rapport d’aspect, calculez la taille ajustée de l’aperçu à partir de l’espace disponible dans la fenêtre, puis donnez cette même hauteur aux commandes. Cet exemple macOS conserve les proportions d’un aperçu synthétique et rend les commandes défilables lorsqu’elles débordent.
La tâche consiste à créer un éditeur de bureau redimensionnable, avec un aperçu à gauche et un panneau de commandes de largeur fixe à droite. Ici, « aperçu » désigne le contenu affiché dans votre application en cours d’exécution. L’objectif est d’aligner les bords supérieurs et inférieurs des deux panneaux lorsque la taille de la fenêtre change.
Quelle hauteur les commandes doivent-elles suivre ?
Commencez par définir le rectangle que vous voulez faire correspondre. Un aperçu ajusté aux proportions peut occuper moins d’espace que le conteneur qui l’entoure. Si les commandes suivent la hauteur du conteneur, elles peuvent dépasser le bas de l’aperçu visible, même lorsque les deux vues sont correctement disposées.
Apple définit le rapport d’aspect comme la largeur divisée par la hauteur. Pour un contenu redimensionnable, aspectRatio(_:contentMode:) avec .fit préserve ce rapport tout en ajustant le contenu à l’espace proposé. L’exemple d’ellipse de la documentation montre pourquoi la forme ajustée et le cadre carré qui l’entoure peuvent avoir des dimensions différentes. Documentation d’Apple sur les rapports d’aspect.
Cette implémentation rend le rectangle ajusté explicite. Elle réserve une largeur fixe aux commandes et un espace entre les panneaux, puis ajuste l’aperçu à la largeur restante et à la hauteur disponible. Les deux panneaux reçoivent la hauteur calculée de l’aperçu.
Il s’agit d’une règle de disposition propre à cet exemple : la hauteur de la zone des commandes dépend de l’aperçu. Une longue liste de commandes défile à l’intérieur de cette zone au lieu d’augmenter la hauteur de l’aperçu. Apple indique que ScrollView affiche une portion déplaçable de son contenu dans une région défilable. Documentation d’Apple sur les vues de défilement.
Calculez une seule fois le rectangle ajusté
Soit W et H les dimensions disponibles du contenu après le padding extérieur, C la largeur des commandes, G l’espace entre les panneaux et R le rapport largeur/hauteur de l’aperçu. Le calcul est le suivant :
remainingWidth = max(0, W - C - G)
previewWidth = min(remainingWidth, H * R)
previewHeight = previewWidth / R
controlsHeight = previewHeight
La première limite empêche l’aperçu d’empiéter sur l’espace horizontal réservé aux commandes. La seconde empêche une fenêtre large de produire un aperçu plus haut que la zone disponible. Calculer la hauteur à partir de la largeur retenue préserve le rapport dans les deux cas.
Par exemple, supposons que la zone de contenu avant le padding mesure 960 par 600 points. Avec un padding de 16 points sur chaque bord, un panneau de commandes de 220 points, un espace de 16 points et un aperçu au format 16:9, la zone disponible mesure 928 par 568 points. Il reste 692 points de largeur pour l’aperçu, qui mesure alors 389,25 points de haut. La zone des commandes mesure donc 220 par 389,25 points.
Ces valeurs sont des données d’exemple, et non des résultats mesurés dans une application. Elles montrent aussi pourquoi faire correspondre les commandes à toute la hauteur disponible de 568 points produirait un mauvais alignement.
Utilisez CGFloat pour les dimensions de mise en page et un rapport positif et fini. En Swift, let ratio: CGFloat = 16.0 / 9.0 rend explicite le calcul en virgule flottante visé. Si votre application accepte ensuite des rapports configurables, validez-les avant la division.
Construisez l’exemple macOS complet
Créez une application SwiftUI pour macOS dans Xcode et définissez sa cible de déploiement sur macOS 13 ou une version ultérieure. Remplacez les déclarations générées pour l’application et la vue de contenu par ce fichier unique, en laissant exactement une déclaration @main dans la cible. L’exemple ne nécessite ni ressources ni paquets externes.
La version minimale requise découle de l’API de dimensionnement de fenêtre utilisée ici. La scène utilise .contentMinSize, qui, selon la documentation d’Apple, déduit la taille minimale de la fenêtre de son contenu sans imposer de taille maximale. Documentation d’Apple sur le redimensionnement des fenêtres, Documentation d’Apple sur la taille minimale du contenu.
import SwiftUI
@main
struct MatchedPreviewApp: App {
var body: some Scene {
WindowGroup {
PreviewEditor()
.frame(minWidth: 640, minHeight: 360)
}
.windowResizability(.contentMinSize)
}
}
struct PreviewEditor: View {
@State private var intensity: Double = 0.5
@State private var showsLabel = true
private let ratio: CGFloat = 16.0 / 9.0
private let controlsWidth: CGFloat = 220
private let gap: CGFloat = 16
var body: some View {
GeometryReader { geometry in
let availableHeight = max(0, geometry.size.height)
let remainingWidth = max(
0,
geometry.size.width - controlsWidth - gap
)
let previewWidth = min(
remainingWidth,
availableHeight * ratio
)
let previewHeight = previewWidth / ratio
HStack(alignment: .top, spacing: gap) {
RoundedRectangle(cornerRadius: 12)
.fill(Color.blue.opacity(0.2 + intensity * 0.7))
.aspectRatio(ratio, contentMode: .fit)
.overlay {
Text("Sample composition")
.font(.headline)
.opacity(showsLabel ? 1 : 0)
}
.frame(
width: previewWidth,
height: previewHeight
)
ScrollView(.vertical) {
VStack(alignment: .leading, spacing: 16) {
Text("Preview controls")
.font(.headline)
Toggle("Show label", isOn: $showsLabel)
VStack(alignment: .leading, spacing: 8) {
Text("Color intensity")
Slider(value: $intensity, in: 0...1)
}
ForEach(1...8, id: \.self) { index in
Text("Sample setting \(index)")
.font(.caption)
}
}
.padding(16)
.frame(maxWidth: .infinity, alignment: .leading)
}
.frame(
width: controlsWidth,
height: previewHeight
)
.background(Color.gray.opacity(0.08))
}
.frame(
width: geometry.size.width,
height: geometry.size.height,
alignment: .center
)
}
.padding(16)
}
}
GeometryReader fournit la taille de la région allouée à l’éditeur. Apple indique qu’il définit le contenu à partir de sa propre taille et de son espace de coordonnées, avec une taille préférée flexible. Placer le padding à l’extérieur du lecteur permet au calcul d’utiliser la zone de contenu après retrait des marges. Documentation d’Apple sur GeometryReader.
Les deux cadres de taille fixe établissent la hauteur commune. Apple explique que frame(width:height:alignment:) crée un cadre invisible aux dimensions spécifiées. L’arrière-plan des commandes vient après ce cadre : il couvre donc toute la zone des commandes, y compris l’espace situé sous une liste courte. Documentation d’Apple sur les cadres de taille fixe.
Le dernier cadre centre la ligne complète dans le lecteur. Il n’agrandit aucun des panneaux. Une fenêtre haute peut donc laisser de l’espace inutilisé au-dessus et au-dessous de la ligne, tout en conservant la même hauteur pour les commandes et l’aperçu.
Préservez l’état et le comportement du cycle de vie lors du redimensionnement
Séparez les valeurs modifiables par l’utilisateur de la géométrie calculée. Dans l’exemple, intensity et showsLabel représentent l’état ; la largeur et la hauteur de l’aperçu sont des valeurs locales dérivées de la taille disponible actuelle.
Apple décrit l’état comme un stockage géré par SwiftUI, dont les changements mettent à jour les parties dépendantes de la hiérarchie des vues. Cela permet de conserver les valeurs du curseur et de l’interrupteur dans l’éditeur qui les possède. Documentation d’Apple sur l’état.
Le calcul de la mise en page ne comporte ni affectation à l’état, ni rappel d’apparition, ni travail asynchrone, ni rétroaction de mesure. Le redimensionnement modifie les dimensions utilisées par la même structure de vues. L’étiquette reste affichée et son opacité change lorsque l’interrupteur est actionné.
Pour adapter cet exemple à un éditeur existant, conservez la propriété des modèles et les types de liaison en place. Appliquez la règle de dimensionnement autour de l’aperçu et des commandes ; ne déplacez pas la création du modèle dans le calcul géométrique. Dans cet exemple, le calcul arithmétique ne nécessite que les dimensions disponibles et le rapport choisi.
Vérifiez le redimensionnement, le débordement et l’ordre des modificateurs
Utilisez les dimensions suivantes pour vérifier les calculs. Elles décrivent la zone de contenu avant le padding extérieur de l’exemple, et non le cadre extérieur de la fenêtre. Les valeurs attendues sont arrondies à deux décimales.
| Zone de contenu | Taille de l’aperçu | Zone des commandes |
|---|---|---|
| 960 × 600 points | 692 × 389,25 | 220 × 389,25 |
| 640 × 360 points | 372 × 209,25 | 220 × 209,25 |
| 1200 × 400 points | 654,22 × 368 | 220 × 368 |
| 700 × 900 points | 432 × 243 | 220 × 243 |
Les calculs ont été vérifiés avec un petit script Python, comprenant des assertions qui vérifient que la hauteur de l’aperçu ajusté reste dans la limite disponible et que les deux panneaux ainsi que l’espace qui les sépare tiennent dans la largeur. Swift et Xcode n’étaient pas disponibles dans l’environnement de recherche ; le programme SwiftUI complet a donc été inspecté, mais ni compilé ni exécuté.
Dans Xcode, lancez l’application et redimensionnez-la en lui donnant des formes larges, basses et hautes. Vérifiez que les bords des panneaux restent alignés, que les commandes défilent lorsque leur contenu dépasse la zone visible et que le curseur et l’interrupteur conservent leurs valeurs pendant le redimensionnement.
Si l’alignement diffère toujours après l’adaptation de l’exemple, examinez les points suivants :
- Padding supplémentaire sur un panneau : le padding appliqué à l’extérieur du cadre de taille fixe d’un seul panneau augmente son encombrement total. Gardez le padding des commandes à l’intérieur de la vue de défilement, comme indiqué.
- Conteneur plus grand autour de l’aperçu : si vous ajoutez un cadre plus grand autour de l’aperçu, déterminez si les commandes doivent correspondre à ce cadre ou au contenu ajusté. Le calcul présenté ici se base sur le rectangle ajusté.
- Contenu personnalisé qui déborde : un cadre définit les dimensions de mise en page, mais ne masque pas automatiquement le dessin qui dépasse. Apple documente
.clipped()pour le rognage rectangulaire ; utilisez-le si vous souhaitez masquer un débordement intentionnel de l’aperçu. Gardez les commandes défilables afin que tous les réglages restent accessibles. Documentation d’Apple sur le rognage. - Espace insuffisant dans le conteneur hôte : l’exemple s’appuie sur sa taille de contenu minimale. Si vous l’intégrez dans un conteneur plus petit, la largeur fixe des commandes risque de ne laisser aucune place utile à l’aperçu. Réduisez la largeur du panneau, adoptez une disposition verticale ou définissez une taille minimale explicite pour le conteneur.
Quand faut-il plutôt utiliser une disposition personnalisée ?
Cette approche convient à un éditeur dont la région de fenêtre est délimitée et qui utilise un rapport d’aspect connu. Sa règle de dimensionnement est explicite et ne dépend pas de la mesure du contenu interne de l’aperçu.
Si votre composant réutilisable doit déterminer sa taille préférée à partir de ses enfants, envisagez un Layout personnalisé. Le contrat documenté par Apple distingue le signalement de la taille composite via sizeThatFits(proposal:subviews:cache:) du positionnement des enfants via placeSubviews(in:proposal:subviews:cache:). Les mandataires de sous-vues fournissent des opérations de mesure et de positionnement. Documentation d’Apple sur les dispositions.
Pour l’éditeur de bureau à espace délimité présenté ici, un rectangle ajusté commun suffit : réservez l’espace horizontal des commandes, ajustez l’aperçu aux deux dimensions disponibles et donnez sa hauteur à la zone des commandes. Vous disposez ainsi d’une règle de redimensionnement claire, tandis que le contenu des commandes reste défilable.
Sources
- aspectRatio(_:contentMode:) — Documentation Apple pour les développeurs.
- ScrollView — Documentation Apple pour les développeurs.
- windowResizability(_:) — Documentation Apple pour les développeurs.
- contentMinSize — Documentation Apple pour les développeurs.
- GeometryReader — Documentation Apple pour les développeurs.
- frame(width:height:alignment:) — Documentation Apple pour les développeurs.
- State — Documentation Apple pour les développeurs.
- clipped(antialiased:) — Documentation Apple pour les développeurs.
- Layout — Documentation Apple pour les développeurs.