Um SwiftUI-Inhalte zu korrigieren, die beim Ändern der Mac-Fenstergröße aus dem sichtbaren Bereich wandern, stelle zunächst fest, ob das Zeichnen über die Layoutgrenzen hinausgeht, Inhalte sich nicht verkleinern lassen oder eine eingebettete AppKit-Ansicht ihrem Container nicht mehr folgt. Korrigiere dann genau diese Grenze.

Diese Anleitung richtet sich an Entwickler, die die skalierbare Benutzeroberfläche ihrer eigenen Mac-App korrigieren möchten: Beschriftungen lesbar halten, Steuerelemente erreichbar machen und längere Inhalte scrollbar gestalten. Die folgende Diagnosefolge verwendet synthetische Beschriftungen und unterscheidet zwischen SwiftUI-Layout, Fenstergröße und Textumbruch in AppKit.

Finde heraus, welche Grenze beim Ändern der Fenstergröße versagt

Das SwiftUI-Layout beginnt mit einem Größenangebot des übergeordneten Elements. Ein untergeordnetes Element wählt daraufhin seine Größe; das Angebot ist keine Anweisung, alles auf dieses Rechteck zu beschneiden. Apple dokumentiert Angebote für minimale, maximale und ideale Größen in ProposedViewSize. Das erklärt, warum ein schmaler gezogenes Fenster nicht zwangsläufig alle untergeordneten Elemente schmaler macht.

Beginne mit einem wiederholbaren Test: Öffne das Fenster deiner App, ziehe es schmaler, bis das erste Problem auftritt, und ziehe es dann wieder breiter. Beobachte das zuerst betroffene Element statt der endgültigen Anordnung. Ragt eine Beschriftung über ihren Container hinaus? Verschiebt sich das ganze Bedienfeld? Behält der Text seinen vorherigen Zeilenumbruch bei?

Füge dem verdächtigen untergeordneten Element vorübergehend .border(Color.orange) und seinem umschließenden Frame oder Container .border(Color.blue) hinzu. Platziere die Modifikatoren auf verschiedenen Ebenen, damit du ihre Rechtecke vergleichen kannst. Apples Dokumentation zu offset zeigt diese Rahmentechnik, mit der sich die ursprüngliche Layoutposition von verschobenem Inhalt unterscheiden lässt.

Beobachtung Zuerst prüfen Sinnvoller nächster Schritt
Das Gezeichnete bewegt sich aus einem ansonsten stabilen Rechteck heraus offset, position, Koordinatenberechnungen Platzierungsmodifikatoren einzeln entfernen
Eine Beschriftung bleibt breit, während ihr Container schmaler wird Horizontales fixedSize, explizite Breiten Der Beschriftung erlauben, eine begrenzte Breite anzunehmen
Steuerelemente passen nicht in eine horizontale Zeile Gesamtbreite der Steuerelemente und Abstände Eine vertikale Alternative anbieten
Unterer Inhalt verschwindet, wenn die Höhe abnimmt Fehlendes Verhalten für überlaufende Inhalte Den Inhaltsbereich scrollbar machen
Ein eingebettetes SwiftUI-Bedienfeld verschiebt sich innerhalb von AppKit Grenzen und Größenbeschränkungen der Hosting-Ansicht Hostgröße mit den Inhaltsanforderungen vergleichen
Eine AppKit-Textansicht wird schmaler, aber die Zeilen bleiben breit Nachführung der Textcontainerbreite Konfiguration von Textansicht und Textcontainer prüfen

Dies sind aus dokumentiertem Verhalten abgeleitete Diagnosehypothesen. Sie garantieren nicht, dass ein bestimmtes Symptom nur eine Ursache hat. Ändere jeweils nur einen Verdacht und wiederhole dieselbe Größenänderung. Wenn das Ersetzen eines komplexen untergeordneten Elements durch Text("Sample panel") das Problem behebt, stelle seine Ebenen nach und nach wieder her.

Entferne Platzierungs- und Größenregeln, die der Fenstergröße entgegenwirken

Prüfe Offsets und zentrierte Positionierung

Ein offset verschiebt den angezeigten Inhalt, ohne die ursprünglichen Abmessungen der Ansicht zu ändern. Ein positiver horizontaler Offset kann die Zeichnung daher über den rechten Rand hinausschieben, obwohl das übergeordnete Element weiterhin den ursprünglichen Platz reserviert. Entferne den Offset vorübergehend. Kehrt die Zeichnung dadurch in den Container zurück, ersetze die Anpassung durch eine Anordnung mit Stacks, Innenabständen oder Frame-Ausrichtung. Siehe Apples Beschreibung des Verhaltens von offset.

position(x:y:) platziert den Mittelpunkt der Ansicht an Koordinaten im Koordinatenraum ihres übergeordneten Elements. Wer diese Koordinaten als obere linke Ecke behandelt, verursacht einen Fehler, der von der Größe des untergeordneten Elements abhängt. Eine einmal berechnete Position kann außerdem ungeeignet werden, wenn sich die Größe des übergeordneten Elements ändert. Für ein gewöhnliches Bedienfeld mit Beschriftungen und Steuerelementen solltest du eine Ausrichtung wählen, die das gewünschte Verhältnis zum Container ausdrückt. Apple beschreibt das zentrierte Verhalten in position(x:y:).

.frame(maxWidth: .infinity, alignment: .leading) drückt zum Beispiel eine führende Ausrichtung innerhalb eines flexiblen Frames aus. Dadurch werden die Größenanforderungen eines zu großen untergeordneten Elements nicht aufgehoben. Apples Dokumentation zu flexiblen Frames erklärt, wie der Frame Angebote begrenzt und sein untergeordnetes Element positioniert.

Prüfe feste Breiten und Texte mit idealer Größe

Prüfe explizite Breiteneinstellungen und horizontale Verwendungen von fixedSize entlang des betroffenen Zweigs. Apple dokumentiert ausdrücklich, dass .fixedSize(horizontal: true, vertical: false) dafür sorgt, dass Text die ideale Breite für die gesamte Zeichenfolge beibehält. Dadurch kann er die Grenzen seines übergeordneten Elements überschreiten. Entferne diese horizontale Anforderung bei umzubrechendem Fließtext. Wenn die vollständige Höhe des umbrochenen Textes erhalten bleiben soll, ziehe .fixedSize(horizontal: false, vertical: true) in Betracht. Siehe fixedSize(horizontal:vertical:).

Wenn die vertikale Größe erhalten bleibt, muss es einen Platz für diese Höhe geben. Ein vertikal wachsender Absatz in einem kurzen, nicht scrollbareren Container kann weiterhin unerreichbar werden. Verwende einen vertikalen Scrollbereich, wenn alle Texte zugänglich sein müssen. Apple dokumentiert die unterstützten Scrollachsen in ScrollView.

Geometriebasierte Berechnungen erneut prüfen

GeometryReader definiert Inhalte anhand seiner eigenen Größe und seines Koordinatenraums und gibt seinem übergeordneten Element eine flexible bevorzugte Größe zurück. Einen solchen Reader um eine Ansicht zu legen, verändert die Layoutstruktur und ist daher keine neutrale Messhülle. Wenn das Wandern nach dem Hinzufügen von Geometrieberechnungen begonnen hat, ersetze diesen Zweig vorübergehend durch gewöhnliche Stacks und Frames. Apple beschreibt diese Eigenschaften in GeometryReader.

Leite bei einem proportionalen Layout, das tatsächlich Geometrie benötigt, die Abmessungen aus dem jeweils aktuellen relevanten Container ab. Prüfe jeden Zustand, in dem eine bei der ersten Anzeige erfasste Breite gespeichert wird. Prüfe auch, ob die gemessene Dimension dazu verwendet wird, denselben Bereich zu skalieren, der gemessen wird. Das Entfernen dieser Abhängigkeit ist ein hilfreicher Isolationstest.

Erstelle ein kompaktes Layout mit festgelegtem Verhalten für überlaufende Inhalte

Das folgende vollständige Beispiel zeigt drei voneinander unabhängige Entscheidungen: Text wird umgebrochen, Steuerelemente wechseln bei Bedarf ihre Anordnung und überlaufende vertikale Inhalte bleiben erreichbar. Die Abmessungen dienen als veranschaulichende Designwerte und sind keine von Apple vorgeschriebenen Werte.

Erstelle in Xcode eine macOS-SwiftUI-App, lege als Bereitstellungsziel macOS 13 oder neuer fest und ersetze die generierten App- und Content-View-Deklarationen durch diese einzelne Datei. Behalte nur eine @main-Deklaration bei. Das Beispiel verwendet ViewThatFits und Fenstergrößen-APIs, die ab macOS 13 verfügbar sind. Apple dokumentiert ihr Verhalten in ViewThatFits und 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 prüft die Alternativen in der angegebenen Reihenfolge und wählt die erste, deren ideale Größe entlang der eingeschränkten Achsen passt. Hier entscheidet die horizontale Anpassung zwischen der Button-Zeile und dem vertikalen Stack. Die Zeile behält bewusst ihre horizontale ideale Größe bei, damit eine zusammengedrückte Zeile nicht zum bevorzugten Ergebnis wird. Beide Alternativen sollten dieselben Aktionen anbieten. Siehe Apples Auswahlregeln.

Der äußere Frame ermöglicht es der Inhaltskolumne, der verfügbaren Breite zu folgen. Die vertikale Scrollansicht sorgt zugleich dafür, dass Inhalte bei wachsender Höhe durch Textumbrüche erreichbar bleiben. Diese Anordnung setzt voraus, dass die einzelnen Steuerelemente bei der gewählten Mindestbreite hineinpassen. Längere Beschriftungen oder größere Schriften erfordern möglicherweise eine andere Mindestbreite oder eine andere kompakte Anordnung.

Dieses Beispiel wurde auf konsistente Deklarationen und API-Verfügbarkeit geprüft; hier wurde es nicht kompiliert, da kein Swift-Compiler verfügbar war. Führe es unter macOS aus, um das tatsächliche Verhalten des Fensters und des Textes zu prüfen.

Lege Fenstergrenzen erst fest, nachdem du das Inhaltslayout korrigiert hast

Eine sinnvolle Mindestfenstergröße verhindert, dass eine Oberfläche kleiner als ihre unterstützte Anordnung wird. Mit .contentMinSize leitet SwiftUI die Mindestgröße des Fensters aus seinen Inhalten ab und legt keine maximale Größe fest. Im Beispiel gibt der Frame an der Wurzel Werte für die Mindestbreite und -höhe vor. Siehe contentMinSize.

Bestimme diese Mindestgröße, indem du die kompakte Anordnung mit den längsten unterstützten Beschriftungen testest. Die Mindestgröße so lange zu erhöhen, bis eine falsch platzierte Ansicht sichtbar wird, kann den ursprünglichen Layoutfehler verdecken. Entferne zuerst unbeabsichtigte Verschiebungen und zu große untergeordnete Elemente. Lege dann die kleinsten Abmessungen fest, bei denen sich die vorgesehene Aufgabe noch erledigen lässt.

Wenn AppKit das Fenster verwaltet und SwiftUI darin einbettet, untersuche die Hosting-Grenze gesondert. Ab macOS 13 steuert NSHostingView.sizingOptions die aus SwiftUI-Inhalten abgeleiteten Größenbeschränkungen. Wenn die Hosting-Ansicht das contentView des Fensters ist, aktualisiert sie außerdem die minimalen und maximalen Inhaltsgrößen des Fensters. Apple dokumentiert zudem, dass Inhalte zentriert werden, wenn der Frame des Hosts von der Größe abweicht, die seine SwiftUI-Inhalte benötigen. Prüfe diese Beziehungen, bevor du kompensierende Offsets hinzufügst. Siehe NSHostingView sizingOptions.

Prüfe AppKit und TextKit nur, wenn die betroffene Ansicht diese APIs verwendet

Lass SwiftUI die Größe der dargestellten Stammansicht bestimmen

Bei einem NSViewRepresentable steuert SwiftUI frame und bounds der dargestellten AppKit-Ansicht. Apple warnt ausdrücklich, dass das manuelle Setzen dieser Eigenschaften mit SwiftUI in Konflikt steht und undefiniertes Verhalten hervorruft. Wenn dein Größenänderungs-Handler wiederholt den Frame der dargestellten Stammansicht zuweist, entferne diese konkurrierende Zuständigkeit und lege die Größe über die SwiftUI-Grenze fest. Siehe NSViewRepresentable.

Wenn eine benutzerdefinierte Messung erforderlich ist, steht ab macOS 13 sizeThatFits(_:nsView:context:) zur Verfügung. Während eines Layoutdurchlaufs kann diese Methode mehrere Angebote erhalten. Mit nil wird die Standardgrößenbestimmung angefordert. Richte das Ergebnis nach dem aktuellen Angebot und den Inhalten aus, nicht nach einer vorherigen Fensterbreite. Für eine nicht angegebene Angebotsdimension brauchst du eine bewusste Messstrategie; behandle sie nicht automatisch als null. Siehe Größenbestimmung für Representables.

Optionale Bindungen in Swift bieten eine Möglichkeit, eine vorgeschlagene Dimension zu prüfen und auszulesen, bevor sie verwendet wird. Das ist nützlich, wenn du eine Größenstrategie implementierst, ohne optionale Breiten oder Höhen erzwungen auszupacken. Dieses Sprachverhalten ist unter The Basics: Optional Binding dokumentiert.

Trenne die Umbruchbreite des Textes von der Breite der Textansicht

Wenn das betroffene untergeordnete Element ein NSTextView ist, vergleiche seine aktuelle Breite mit der Breite seines Textcontainers. Apple dokumentiert, dass NSTextContainer.widthTracksTextView standardmäßig false ist. Wenn du den Wert auf true setzt, passt der Container seine Breite an, sobald sich die Breite der Textansicht ändert. Diese Einstellung ist entscheidend, wenn die Zeilen mit der Textansicht neu umbrechen sollen. Siehe widthTracksTextView.

Prüfe außerdem isHorizontallyResizable. Diese Eigenschaft steuert, ob die Textansicht ihre Breite so ändert, dass ihr Text hineinpasst. Das ist eine andere Anforderung als Text innerhalb einer vorgegebenen Breite umzubrechen. Richte bei einem vertikal scrollbareren Textbereich mit Umbruch beide Einstellungen konsistent ein und stelle sicher, dass das umschließende Layout die Größe der Textansicht tatsächlich anpasst. Siehe isHorizontallyResizable.

Behalte die Text-Engine bei, die deine Implementierung verwendet. Apple dokumentiert, dass der Zugriff auf layoutManager bei einem NSTextView oder seinem Textcontainer in einen Kompatibilitätsmodus mit NSLayoutManager wechseln kann. Füge diesen Zugriff bei der Diagnose einer TextKit-2-Ansicht nicht unbedacht hinzu. Verwende die passenden APIs für die vorhandene Engine und das Bereitstellungsziel. Siehe NSTextView.

Überprüfe die Korrektur bei Änderungen von Breite und Höhe

Führe in deiner eigenen App die folgende abschließende Prüfung durch:

  1. Ziehe das Fenster langsam schmaler, bis es den Punkt erreicht, an dem das ursprüngliche Problem auftrat, und ziehe es dann wieder breiter.
  2. Verringere unabhängig davon die Höhe und prüfe, ob alle benötigten Inhalte weiterhin erreichbar sind.
  3. Wiederhole den Test mit langen synthetischen Beschriftungen und mehrzeiligem Text.
  4. Aktiviere jedes Steuerelement sowohl in der breiten als auch in der kompakten Anordnung.
  5. Prüfe bei einer AppKit-Textansicht, ob der Umbruch bei Größenänderungen in beide Richtungen der aktuellen Breite folgt.

Eine gelungene Korrektur verankert die Platzierung an den aktuellen Container, bietet den Inhalten eine brauchbare kompakte Anordnung und macht überlaufende Inhalte erreichbar. Sobald sich dieses Verhalten bei wiederholten Größenänderungen bestätigt, entferne die vorübergehend hinzugefügten Rahmen und behalte die durch die Oberfläche gerechtfertigte Mindestfenstergröße bei.

Quellen