Damit Overlays an einem mit Aspect Fit angepassten SwiftUI-Diagramm ausgerichtet bleiben, messen Sie das angepasste Diagramm selbst und platzieren Sie Markierungen in diesem lokalen Koordinatenraum. Fügen Sie das messende Overlay vor jedem äußeren Frame hinzu, der Leerraum erzeugt.
In diesem Tutorial erstellen Sie eine kleine macOS-App mit einem synthetischen Diagramm und zwei nummerierten Markierungen. Die Aufgabe besteht darin, jede Markierung mittig über dem jeweiligen Diagrammmerkmal zu halten, während sich die Form des Fensters ändert. Die Implementierung verwendet normalisierte Koordinaten, sodass die Positionen der Beschriftungen unabhängig von den Fensterabmessungen bleiben.
Warum verschieben sich Overlays gegenüber einem mit Aspect Fit angepassten Diagramm?
Aspect Fit bewahrt die Proportionen eines Diagramms und passt es in den verfügbaren Platz ein. Je nach Seitenverhältnis des Containers kann dadurch oberhalb und unterhalb des Diagramms oder an seinen Seiten Leerraum entstehen. Apple veranschaulicht dieses Verhalten unter Bilder in den verfügbaren Platz einpassen.
Die Verschiebung entsteht, wenn die Grafik und ihre Markierungen unterschiedliche Rechtecke verwenden. Die Grafik könnte beispielsweise ein zentriertes Rechteck mit 600 × 375 Punkten innerhalb eines 600 × 700 Punkte großen Containers belegen. Wird eine Markierung anhand eines Bruchteils der Containerhöhe positioniert, fließt der umgebende Leerraum in die Berechnung ein.
Die Reihenfolge der Modifikatoren bestimmt, welches Rechteck Sie messen. Apple dokumentiert, dass View-Modifikatoren die empfangende View umschließen und ein Frame sein untergeordnetes Element innerhalb eines anderen Rechtecks positioniert. Ein Overlay, das nach einem vergrößernden Frame angefügt wird, misst daher eine andere Layoutgrenze als ein Overlay, das direkt an der angepassten Grafik angebracht ist. Siehe Apples View-Dokumentation und Dokumentation zu flexiblen Frames.
Verwenden Sie für diese Aufgabe die folgende Reihenfolge:
- Zeichnen Sie flexible Grafiken.
- Wenden Sie mit
.fitdas explizite Seitenverhältnis an. - Fügen Sie ein Overlay mit
GeometryReaderhinzu. - Positionieren Sie die Markierungen anhand der Größe dieses Readers.
- Fügen Sie den äußeren Frame und den umgebenden Innenabstand hinzu.
Apple empfiehlt ein Overlay, wenn die modifizierte View das Layout bestimmen soll. Damit eignet sich das Diagramm als maßgeblicher Layoutinhalt, über den die Beschriftungen gelegt werden. Eine Anordnung gleichrangiger Elemente in einem ZStack verhält sich beim Layout anders; dabei muss sorgfältiger darauf geachtet werden, welche View die Größe bestimmt. Siehe overlay(alignment:content:).
Ein vollständiges macOS-Beispiel erstellen
Erstellen Sie in Xcode mit der Oberfläche SwiftUI ein macOS-App-Projekt. Legen Sie das Deployment-Ziel auf macOS 12 oder neuer fest, da die hier verwendete Closure-basierte Overlay-API ab macOS 12 verfügbar ist. Ersetzen Sie die generierte Datei mit dem App-Einstiegspunkt durch das folgende vollständige Programm. Im Ziel darf nur eine @main-App-Deklaration vorhanden sein. Die Verfügbarkeit der API ist unter overlay(alignment:content:) dokumentiert.
Die synthetische Grafik bildet einen Entwurfsbereich von 800 × 500 ab und hat damit ein Seitenverhältnis von 1,6. Die Markierungskoordinaten sind Bruchteile dieses Bereichs: Horizontal steht null für die linke Kante, vertikal steht null für die obere Kante und eins für die jeweils gegenüberliegende Kante. Asset-Dateien oder externe Pakete sind nicht erforderlich.
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
}
}
Starten Sie die App und ändern Sie die Fenstergröße sowohl horizontal als auch vertikal. Markierung 1 sollte in der Mitte des ersten Rechtecks bleiben und Markierung 2 in der Mitte des zweiten. Der dünne Rahmen kennzeichnet die Grenze des angepassten Diagramms; der graue Bereich darum gehört zum äußeren Layout.
Warum diese Reihenfolge der Modifikatoren funktioniert
Das Basisrechteck stellt flexible Grafiken bereit, und .aspectRatio(800.0 / 500.0, contentMode: .fit) legt ihre Proportionen fest. Apple definiert das Argument für das Seitenverhältnis als Breite geteilt durch Höhe und gibt an, dass das angepasste Ergebnis auf skalierbare Inhalte angewendet wird. Siehe aspectRatio(_:contentMode:).
Direkt nach dieser Einschränkung folgt das messende Overlay. GeometryReader bestimmt seinen Inhalt anhand seiner eigenen Größe und seines eigenen Koordinatenraums, während geometry.size die Größe des Containers zurückgibt. Hier umfasst dieser Container das angepasste Rechteck. Multipliziert man die normalisierten Koordinaten mit seiner Breite und Höhe, erhält man daher die lokalen Positionen der Markierungen. Siehe GeometryReader und GeometryProxy.size.
Der abschließende vergrößernde Frame zentriert die fertige Kombination aus Diagramm und Markierungen im verfügbaren Bereich. Da die Markierungen bereits am inneren Rechteck angebracht sind, müssen sie die äußeren Ränder nicht berechnen.
Zeichen- und Positionierungsverträge einhalten
DiagramArtwork entspricht dem Protokoll Shape, und seine Methode path(in:) gibt einen Path zurück, der innerhalb des übergebenen Rechtecks definiert ist. Sie gibt keine View zurück und nimmt keine Änderungen am Layout vor. Das entspricht Apples dokumentiertem path(in:)-Vertrag.
Die Methode .position(x:y:) jeder Markierung platziert ihren Mittelpunkt am berechneten Punkt. Sie platziert dort nicht die obere linke Ecke. Im Beispiel wird bewusst der Mittelpunkt jedes nummerierten Kreises an einem Merkmal ausgerichtet, wie es Apples Definition von position(x:y:) vorsieht.
Beim Ändern der Fenstergröße bleiben die Kreise 28 Punkte breit, während ihre Mittelpunkte der Grafik folgen. Das ist eine bewusste Darstellungsentscheidung, damit die Beschriftungen gut lesbar bleiben. Wenn die Abmessungen der Markierungen Entfernungen innerhalb der Zeichnung darstellen, berechnen Sie auch diese Abmessungen anhand der angepassten Größe.
Ausrichtung anhand einer Koordinatenberechnung prüfen
Eine numerische Überprüfung hilft dabei, einen Koordinatenfehler von einem Stilproblem zu unterscheiden. Für eine zentrierte Anpassung mit Aspect Fit seien die positiven Quelldimensionen sourceWidth und sourceHeight sowie die endlichen verfügbaren Abmessungen containerWidth und containerHeight. Das Folgende ist mathematischer Pseudocode, kein separates Swift-Programm:
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
Diese Gleichungen leiten anhand des dokumentierten Anpassungsverhaltens eine gleichmäßige Skalierung und zentrierte Platzierung ab. Für die beispielhaften Quelldaten von 800 × 500 und die Markierung 1 bei (0.25, 0.40) ergeben sich folgende Werte. Die Containerabmessungen beziehen sich hier auf den nach Abzug des umgebenden Innenabstands verfügbaren Bereich.
| Verfügbarer Container | Angepasstes Diagramm | Ursprung des Diagramms im Container | Mittelpunkt der Markierung im Container |
|---|---|---|---|
| 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) |
In der mittleren Zeile ergibt die Multiplikation der gesamten Containerhöhe mit 0.40 den Wert 280. Damit verfehlt man die korrekte vertikale Position 312,5. Im lokalen Overlay des Beispiels lautet die vertikale Koordinate einfach 0.40 × 375 = 150; das umgebende Layout stellt den Rand von 162,5 Punkten bereit.
Die Berechnungen in der Tabelle wurden mit einer kleinen deterministischen Python-Berechnung überprüft. Das SwiftUI-Programm wurde geprüft, in dieser Umgebung aber nicht kompiliert. Überprüfen Sie die Darstellung in Xcode auf dem Ziel-Mac.
Das Muster an ein vorhandenes Diagramm anpassen
Machen Sie bei einem bildbasierten Diagramm das Image skalierbar, bevor Sie die Modifikatoren für Aspect Fit und Overlay anwenden. Apples Anleitung zum Einpassen von Bildern erklärt, dass ein Bild andernfalls in seiner Originalgröße dargestellt wird und dass allein .resizable() seine Achsen unabhängig voneinander skalieren kann. Bewahren Sie das tatsächliche Seitenverhältnis des Bildes und fügen Sie das Overlay für die Markierungen vor dem äußeren Frame an. Siehe Bilder in den verfügbaren Platz einpassen.
Wenn die Beschriftungen zunächst in Quellkoordinaten vorliegen, normalisieren Sie sie anhand derselben Gesamtgrenzen der Grafik: Teilen Sie die horizontale Quellkoordinate durch die Quellbreite und die vertikale Quellkoordinate durch die Quellhöhe. Transparente Ränder innerhalb eines Bildes gehören weiterhin zu diesen Grenzen. Wurden Koordinaten anhand eines zugeschnittenen Bereichs gemessen, wandeln Sie sie vor Verwendung dieses Musters in das Koordinatensystem des vollständigen Bildes um.
Behalten Sie bildspezifische Modifikatoren am Image, bevor allgemeine View-Modifikatoren es umschließen. Verwenden Sie für die zusammengesetzte View weiterhin var body: some View und für die Zeichenmethode der Shape weiterhin den Rückgabetyp Path. Apple dokumentiert die Zusammensetzung benutzerdefinierter Views und das Umschließen durch Modifikatoren unter View.
Die Berechnung setzt eine gleichmäßige Anpassung ohne zusätzlichen Zuschnitt, Drehung oder unabhängige Schwenk- und Zoomtransformation voraus. Wenn Sie solche Operationen hinzufügen, wenden Sie dieselbe Transformation auf Grafik und Beschriftungen an oder erweitern Sie die Koordinatenabbildung ausdrücklich. Ein Wechsel zu .fill ändert die Anpassungsbedingungen und kann Inhalte über den Frame hinaus darstellen. Apple dokumentiert das Zuschneiden als separaten Vorgang unter Bilder in den verfügbaren Platz einpassen.
Verbleibende Ausrichtungsprobleme beheben
Prüfen Sie die folgenden Punkte der Reihe nach:
- Messgrenze: Lassen Sie das Overlay für die Markierungen vor dem äußeren vergrößernden Frame. Sein Rahmen sollte der Grafik folgen und nicht dem umgebenden Leerraum.
- Koordinatengrenzen: Vergewissern Sie sich, dass die normalisierten Koordinaten den vollständigen Quellbereich und den vorgesehenen Ursprung oben links verwenden. In diesem Beispiel reichen die vorgesehenen Werte innerhalb der Grenzen von null bis eins. Unerwartete Werte sollten zurückgewiesen und nicht unbemerkt begrenzt werden.
- Ankerpunkt: Denken Sie daran, dass
.position(x:y:)den Mittelpunkt der Markierung als Anker verwendet. Soll eine Ecke einer Beschriftung ein Merkmal berühren, ist ein zusätzlicher expliziter Versatz oder eine andere Verankerungsstrategie erforderlich. - Kleine Layouts: Eine Markierung kann korrekt zentriert bleiben und dennoch eine andere Markierung überdecken. Legen Sie eine separate Regel fest, nach der Beschriftungen bei kleinen Größen ausgeblendet oder neu angeordnet werden.
- Randverhalten: Ein an einer Kante der Grafik zentrierter Marker ragt über diese Kante hinaus. Entscheiden Sie, ob dieser Überstand akzeptabel ist, bevor Sie einen Zuschnitt hinzufügen.
Leiten Sie die Positionen stets aus der aktuellen Geometrie ab, statt die zuerst gemessene Größe in .onAppear zu speichern. Apple beschreibt diesen Callback im Zusammenhang mit dem Erscheinen einer View. Der Zeitpunkt hängt vom View-Typ ab; es handelt sich nicht um einen Callback für Größenänderungen des Fensters. Wird die Geometrie direkt ausgelesen, bleibt die Positionsberechnung dieses Beispiels an das aktuelle Layout gebunden. Siehe onAppear(perform:).
Für ein angepasstes Diagramm in einem Mac-Fenster mit veränderbarer Größe ist ein gemeinsames Rechteck der zuverlässige Ausgangspunkt: Lassen Sie die Grafik ihre angepassten Grenzen festlegen, messen Sie diese Grenzen und leiten Sie daraus alle Mittelpunkte der Beschriftungen ab. Fügen Sie das umgebende Layout erst hinzu, wenn diese Beziehung feststeht.
Quellen
- aspectRatio(_:contentMode:) — Apple Developer Documentation.
- overlay(alignment:content:) — Apple Developer Documentation.
- GeometryReader — Apple Developer Documentation.
- size — Apple Developer Documentation.
- position(x:y:) — Apple Developer Documentation.
- Bilder in den verfügbaren Platz einpassen — Apple Developer Documentation.
- frame(minWidth:idealWidth:maxWidth:minHeight:idealHeight:maxHeight:alignment:) — Apple Developer Documentation.
- path(in:) — Apple Developer Documentation.
- View — Apple Developer Documentation.
- onAppear(perform:) — Apple Developer Documentation.