Pour garder les superpositions alignées sur un diagramme SwiftUI ajusté selon son rapport hauteur/largeur, mesurez le diagramme ajusté lui-même et placez les marqueurs dans son espace de coordonnées local. Ajoutez la superposition de mesure avant tout cadre extérieur qui ajoute de l’espace vide.
Ce tutoriel crée une petite app macOS avec un diagramme synthétique et deux marqueurs numérotés. Le but est de garder chaque marqueur centré sur l’élément correspondant du diagramme lorsque la forme de la fenêtre change. L’implémentation utilise des coordonnées normalisées, de sorte que les positions des annotations restent indépendantes des dimensions de la fenêtre.
Pourquoi les superpositions dérivent-elles par rapport à un diagramme ajusté selon son rapport hauteur/largeur ?
L’ajustement selon le rapport hauteur/largeur préserve les proportions d’un diagramme tout en le faisant tenir dans l’espace disponible. Selon les proportions du conteneur, cela peut laisser de l’espace vide au-dessus et en dessous du diagramme, ou sur ses côtés. Apple illustre ce comportement dans Ajuster des images à l’espace disponible.
La dérive se produit lorsque l’illustration et ses marqueurs utilisent des rectangles différents. Par exemple, l’illustration peut occuper un rectangle centré de 600 par 375 points dans un conteneur de 600 par 700 points. Un marqueur positionné à l’aide d’une fraction de la hauteur du conteneur inclut l’espace vide environnant dans son calcul.
L’ordre des modificateurs détermine le rectangle mesuré. Apple indique que les modificateurs de vue enveloppent la vue à laquelle ils sont appliqués et qu’un cadre positionne son enfant dans un autre rectangle. Une superposition ajoutée après un cadre extensible mesure donc une limite de disposition différente de celle d’une superposition ajoutée directement à l’illustration ajustée. Consultez la documentation View d’Apple et la documentation sur les cadres flexibles.
Pour cette tâche, respectez l’ordre suivant :
- Dessinez une illustration flexible.
- Appliquez son rapport hauteur/largeur explicite avec
.fit. - Ajoutez une superposition contenant
GeometryReader. - Positionnez les marqueurs à l’aide de la taille fournie par ce lecteur.
- Ajoutez le cadre extérieur et les marges environnantes.
Apple recommande une superposition lorsque la vue modifiée doit déterminer la disposition. Le diagramme peut ainsi piloter la disposition, les annotations étant placées par-dessus. Une disposition de vues sœurs dans un ZStack a un comportement différent et exige de prêter davantage attention à la vue qui détermine la taille. Consultez overlay(alignment:content:).
Créer un exemple macOS complet
Créez un projet d’app macOS dans Xcode en utilisant l’interface SwiftUI. Définissez la cible de déploiement sur macOS 12 ou une version ultérieure, car l’API de superposition avec fermeture utilisée ici est disponible à partir de macOS 12. Remplacez le fichier de point d’entrée de l’app généré par le programme complet suivant, en ne gardant qu’une seule déclaration d’app @main dans la cible. La disponibilité de l’API est documentée dans overlay(alignment:content:).
L’illustration synthétique représente un espace de conception de 800 par 500, soit un rapport hauteur/largeur de 1,6. Les coordonnées des marqueurs sont des fractions de cet espace : zéro à l’horizontale correspond au bord gauche, zéro à la verticale correspond au bord supérieur, et un correspond au bord opposé. Aucun fichier de ressources ni package externe n’est nécessaire.
import SwiftUI
@main
struct DiagramAlignmentApp: App {
var body: some Scene {
WindowGroup {
DiagramExample()
.frame(minWidth: 360, minHeight: 260)
}
}
}
struct DiagramMarker: Identifiable {
let id: Int
let label: String
let x: CGFloat
let y: CGFloat
}
struct DiagramExample: View {
let markers = [
DiagramMarker(id: 1, label: "1", x: 0.25, y: 0.40),
DiagramMarker(id: 2, label: "2", x: 0.75, y: 0.60)
]
var body: some View {
Rectangle()
.fill(Color.white)
.overlay {
DiagramArtwork()
.stroke(Color.gray, lineWidth: 2)
}
.aspectRatio(800.0 / 500.0, contentMode: .fit)
.overlay {
GeometryReader { geometry in
ForEach(markers) { marker in
Circle()
.fill(Color.blue)
.frame(width: 28, height: 28)
.overlay {
Text(marker.label)
.font(.caption.bold())
.foregroundColor(.white)
}
.position(
x: marker.x * geometry.size.width,
y: marker.y * geometry.size.height
)
}
}
}
.border(Color.gray)
.frame(
maxWidth: .infinity,
maxHeight: .infinity,
alignment: .center
)
.padding(24)
.background(Color.gray.opacity(0.12))
}
}
struct DiagramArtwork: Shape {
func path(in rect: CGRect) -> Path {
var path = Path()
path.addRect(CGRect(
x: rect.minX + 0.10 * rect.width,
y: rect.minY + 0.20 * rect.height,
width: 0.30 * rect.width,
height: 0.40 * rect.height
))
path.addRect(CGRect(
x: rect.minX + 0.60 * rect.width,
y: rect.minY + 0.40 * rect.height,
width: 0.30 * rect.width,
height: 0.40 * rect.height
))
path.move(to: CGPoint(
x: rect.minX + 0.25 * rect.width,
y: rect.minY + 0.40 * rect.height
))
path.addLine(to: CGPoint(
x: rect.minX + 0.75 * rect.width,
y: rect.minY + 0.60 * rect.height
))
return path
}
}
Lancez l’app et redimensionnez la fenêtre horizontalement et verticalement. Le marqueur 1 doit rester au centre du premier rectangle, et le marqueur 2 au centre du second. La fine bordure délimite le diagramme ajusté ; la zone grise environnante fait partie de la disposition extérieure.
Pourquoi cet ordre de modificateurs fonctionne
Le rectangle de base fournit une illustration flexible, et .aspectRatio(800.0 / 500.0, contentMode: .fit) en contraint les proportions. Apple définit l’argument du rapport hauteur/largeur comme le quotient de la largeur par la hauteur et précise que le résultat ajusté s’applique au contenu redimensionnable. Consultez aspectRatio(_:contentMode:).
La superposition de mesure vient immédiatement après cette contrainte. GeometryReader définit son contenu à partir de sa propre taille et de son espace de coordonnées, tandis que geometry.size indique la taille du conteneur. Ici, ce conteneur couvre le rectangle ajusté. Multiplier les coordonnées normalisées par sa largeur et sa hauteur donne donc les positions locales des marqueurs. Consultez GeometryReader et GeometryProxy.size.
Le cadre extensible final centre la composition du diagramme et de ses marqueurs dans l’espace disponible. Comme les marqueurs sont déjà rattachés au rectangle intérieur, ils n’ont pas besoin de calculer les marges extérieures.
Préserver les contrats de dessin et de positionnement
DiagramArtwork est conforme à Shape, et sa méthode path(in:) renvoie un Path défini dans le rectangle fourni. Elle ne renvoie pas de vue et ne modifie pas la disposition. Cela respecte le contrat documenté de path(in:) d’Apple.
La méthode .position(x:y:) de chaque marqueur place son centre au point calculé. Elle ne place pas son coin supérieur gauche à cet endroit. L’exemple ancre volontairement le centre de chaque cercle numéroté à un élément du diagramme, conformément à la définition de position(x:y:) d’Apple.
Les cercles restent larges de 28 points lorsque la fenêtre est redimensionnée, tandis que leur centre suit l’illustration. Ce choix de présentation permet de garder les annotations lisibles. Si les dimensions des marqueurs représentent des distances dans le dessin, calculez-les également à partir de la taille ajustée.
Vérifier l’alignement avec un calcul de coordonnées détaillé
Un contrôle numérique permet de distinguer une erreur de coordonnées d’un problème de style. Pour un ajustement centré selon le rapport hauteur/largeur, définissez les dimensions sources positives comme sourceWidth et sourceHeight, et les dimensions disponibles finies comme containerWidth et containerHeight. Voici du pseudocode mathématique, et non un programme Swift distinct :
scale = min(containerWidth / sourceWidth,
containerHeight / sourceHeight)
fittedWidth = sourceWidth * scale
fittedHeight = sourceHeight * scale
originX = (containerWidth - fittedWidth) / 2
originY = (containerHeight - fittedHeight) / 2
containerX = originX + normalizedX * fittedWidth
containerY = originY + normalizedY * fittedHeight
Ces équations déduisent un ajustement uniforme et un placement centré à partir du comportement documenté de l’ajustement. Avec la source illustrative de 800 par 500 et le marqueur 1 en (0.25, 0.40), on obtient les résultats suivants. Les dimensions du conteneur indiquées ici correspondent à l’espace disponible après les marges environnantes.
| Conteneur disponible | Diagramme ajusté | Origine du diagramme dans le conteneur | Centre du marqueur dans le conteneur |
|---|---|---|---|
| 1000 × 700 | 1000 × 625 | (0, 37.5) | (250, 287.5) |
| 600 × 700 | 600 × 375 | (0, 162.5) | (150, 312.5) |
| 1000 × 400 | 640 × 400 | (180, 0) | (340, 160) |
Dans la ligne du milieu, multiplier la hauteur totale du conteneur par 0.40 donne 280, ce qui ne correspond pas à la position verticale correcte de 312.5. Dans la superposition locale de l’exemple, la coordonnée verticale est simplement 0.40 × 375 = 150 ; la disposition englobante fournit la marge de 162.5 points.
Les calculs du tableau ont été vérifiés à l’aide d’un petit calcul Python déterministe. Le programme SwiftUI a été examiné, mais n’a pas été compilé dans cet environnement ; vérifiez son rendu dans Xcode sur le Mac cible.
Adapter le modèle à un diagramme existant
Pour un diagramme basé sur une image, rendez l’Image redimensionnable avant d’appliquer les modificateurs d’ajustement selon le rapport hauteur/largeur et de superposition. Le guide d’Apple sur l’ajustement des images explique qu’une image s’affiche autrement à sa taille d’origine et que .resizable() à lui seul peut redimensionner indépendamment ses axes. Préservez le rapport hauteur/largeur réel de l’image, puis ajoutez la superposition des marqueurs avant le cadre extérieur. Consultez Ajuster des images à l’espace disponible.
Lorsque les annotations sont initialement définies par des coordonnées sources, normalisez-les par rapport aux limites de l’illustration entière : divisez la coordonnée horizontale source par la largeur de la source et la coordonnée verticale source par sa hauteur. Les marges transparentes à l’intérieur d’une image font partie de ces limites. Si les coordonnées ont été mesurées par rapport à une zone recadrée, convertissez-les dans le système de coordonnées de l’image entière avant d’utiliser ce modèle.
Gardez les modificateurs propres à l’image sur l’Image avant que les modificateurs généraux de vue ne l’enveloppent. De même, conservez var body: some View pour la vue composée et le type de retour Path pour la méthode de dessin de la forme. Apple documente la composition de vues personnalisées et l’enveloppage par des modificateurs dans View.
Le calcul suppose un ajustement uniforme, sans recadrage supplémentaire, rotation, ni transformation indépendante de panoramique et de zoom. Si vous introduisez ces opérations, appliquez une transformation commune à l’illustration et aux annotations, ou étendez explicitement la conversion des coordonnées. Passer à .fill modifie les conditions d’ajustement et peut afficher du contenu au-delà du cadre ; Apple documente le masquage comme une opération distincte dans Ajuster des images à l’espace disponible.
Diagnostiquer les problèmes d’alignement persistants
Vérifiez les points suivants dans l’ordre :
- Limite de mesure : gardez la superposition des marqueurs avant le cadre extérieur extensible. Sa bordure doit suivre l’illustration plutôt que la zone vide environnante.
- Limites des coordonnées : vérifiez que les coordonnées normalisées utilisent le rectangle source entier et l’origine voulue en haut à gauche. Pour cet exemple, les valeurs attendues à l’intérieur des limites vont de zéro à un ; rejetez les valeurs inattendues au lieu de les borner silencieusement.
- Choix du point d’ancrage : n’oubliez pas que
.position(x:y:)ancre le centre du marqueur. Si le coin d’une étiquette doit toucher un élément, ajoutez explicitement un décalage ou utilisez une autre stratégie d’ancrage. - Petites dispositions : un marqueur peut rester correctement centré tout en chevauchant un autre marqueur. Définissez une règle distincte pour masquer ou réorganiser les étiquettes lorsque l’espace est réduit.
- Comportement aux bords : un marqueur centré sur le bord d’une illustration dépasse de ce bord. Déterminez si ce dépassement est acceptable avant d’ajouter un masquage.
Calculez les positions à partir de la géométrie actuelle au lieu d’enregistrer la première taille mesurée dans .onAppear. Apple définit ce rappel autour de l’apparition, dont le moment dépend du type de vue ; il ne s’agit pas d’un rappel de redimensionnement de fenêtre. La lecture directe de la géométrie lie le calcul de positionnement de cet exemple à la disposition actuelle. Consultez onAppear(perform:).
Pour un diagramme ajusté dans une fenêtre Mac redimensionnable, partez d’un seul rectangle commun : laissez l’illustration définir ses limites ajustées, mesurez-les et calculez le centre de chaque annotation à partir de ces limites. Ajoutez la disposition environnante seulement après avoir établi cette relation.
Sources
- aspectRatio(_:contentMode:) — Documentation pour développeurs Apple.
- overlay(alignment:content:) — Documentation pour développeurs Apple.
- GeometryReader — Documentation pour développeurs Apple.
- size — Documentation pour développeurs Apple.
- position(x:y:) — Documentation pour développeurs Apple.
- Ajuster des images à l’espace disponible — Documentation pour développeurs Apple.
- frame(minWidth:idealWidth:maxWidth:minHeight:idealHeight:maxHeight:alignment:) — Documentation pour développeurs Apple.
- path(in:) — Documentation pour développeurs Apple.
- View — Documentation pour développeurs Apple.
- onAppear(perform:) — Documentation pour développeurs Apple.