Damit eine SwiftUI-Scroll-View zu kurzen Inhalten passt, messen Sie ihren inneren Inhalt und setzen die Höhe des Anzeigebereichs auf den kleineren Wert aus dieser Messung und einem festgelegten Grenzwert. Längere Inhalte lassen sich dann innerhalb dieses Grenzwerts scrollen. In dieser Anleitung richten Sie dieses Verhalten für ein kleines Beschriftungsfeld in Ihrer eigenen Mac-App ein.
Warum bleibt bei einer kurzen Scroll-View leerer Platz?
Unterscheiden Sie zunächst zwei Maße: die Höhe des Inhalts und die Höhe des Anzeigebereichs, in dem er dargestellt wird. Apple beschreibt ScrollView als Ansicht eines Teils des zugrunde liegenden Inhalts, der beim Scrollen angepasst wird. Eine kurze Gruppe von Beschriftungen legt für sich genommen nicht die gewünschte Höhe des Anzeigebereichs fest. Apples Dokumentation zu ScrollView erläutert diesen Unterschied.
Legen Sie für ein kompaktes Desktop-Panel eine explizite Größenregel fest: viewportHeight = min(contentHeight, maximumHeight). Messen Sie den Inhalt mit der tatsächlich verfügbaren Breite, einschließlich aller Innenabstände, die mitgescrollt werden sollen. Wenden Sie das Ergebnis auf die äußere Scroll-View an. Swifts min(_:_:) gibt den kleineren Wert zurück, während SwiftUIs frame(width:height:alignment:) eine feste Höhe festlegt, wenn ein Höhenargument angegeben wird. Swift min(_:_:), Fester SwiftUI-Rahmen.
Nehmen wir zur Veranschaulichung an, der Höchstwert beträgt 240 Punkte. Bei einem Inhalt mit einer Höhe von 86 Punkten ergibt sich ein 86 Punkte hoher Anzeigebereich; bei einem Inhalt mit einer Höhe von 410 Punkten beträgt die Höhe des Anzeigebereichs 240 Punkte, und der übrige Inhalt ist durch Scrollen erreichbar. Dies sind beispielhafte Werte, keine Messungen aus dem folgenden Beispiel.
| Änderung | Was sie steuert | Legt sie die gewünschte kompakte Höhe fest? |
|---|---|---|
| Gemessene Inhaltshöhe, begrenzt und auf die Scroll-View angewendet | Höhe des Anzeigebereichs | Ja, im unten gezeigten Layout mit Begrenzung |
.scrollBounceBehavior(.basedOnSize) |
Rücksprungverhalten abhängig von der Inhaltsgröße | Nein |
.fixedSize(horizontal: false, vertical: true) für die Beschriftungen |
Ideale vertikale Größe des Inhalts | Unterstützt die Messung; für den äußeren Anzeigebereich ist weiterhin eine Höhenregel erforderlich |
Apple beschreibt .basedOnSize als Rücksprungkonfiguration und fixedSize(horizontal:vertical:) als Fixierung einer Ansicht auf ihre ideale Größe entlang ausgewählter Dimensionen. Keine der beiden Beschreibungen legt eine aus dem Inhalt abgeleitete Höhe des Anzeigebereichs fest. Scroll-Rücksprungverhalten, Feste Größe nach Achse.
Ein kompaktes Panel mit maximaler Scroll-Höhe erstellen
Verwenden Sie eine aktuelle Xcode-Version, deren macOS-SDK die einwertige Überladung von onGeometryChange(for:of:action:) bereitstellt. Apple dokumentiert diese Überladung derzeit als ab macOS 13.0 verfügbar. Das vollständige Beispiel unten setzt macOS 13.3 oder neuer voraus, da es auch scrollBounceBehavior(_:axes:) verwendet, das ab macOS 13.3 verfügbar ist. Geometrieänderungs-Callback, Verfügbarkeit des Rücksprung-Modifikators.
Erstellen Sie ein macOS-App-Projekt mit SwiftUI. Ersetzen Sie die generierte App-Deklaration und die Inhaltsansicht durch dieses Programm in einer einzelnen Datei. Achten Sie darauf, dass das Ziel nur eine @main-Deklaration enthält. Alle Beschriftungen sind künstliche lokale Zeichenfolgen. Mit dem Schalter Show more labels wechseln Sie zwischen kurzen und langen Inhalten.
import SwiftUI
@main
struct CompactLabelsApp: App {
var body: some Scene {
WindowGroup {
CompactLabelsView()
}
}
}
struct CompactLabelsView: View {
@State private var showMore = false
@State private var viewportHeight: CGFloat = 240
private var labels: [String] {
if showMore {
return (1...18).map { number in
"Sample label \(number): a longer description that can wrap when the panel becomes narrower."
}
}
return ["Sample label A", "Sample label B"]
}
var body: some View {
VStack(alignment: .leading, spacing: 12) {
Toggle("Show more labels", isOn: $showMore)
ScrollView(.vertical) {
VStack(alignment: .leading, spacing: 8) {
if labels.isEmpty {
Text("No labels to display")
} else {
ForEach(labels.indices, id: \.self) { index in
Text(labels[index])
}
}
}
.font(.body)
.fixedSize(horizontal: false, vertical: true)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(12)
.onGeometryChange(for: CGFloat.self) { proxy in
min(proxy.size.height.rounded(.up), 240)
} action: { newHeight in
viewportHeight = newHeight
}
}
.frame(height: viewportHeight)
.scrollBounceBehavior(.basedOnSize, axes: .vertical)
.border(Color.secondary.opacity(0.4))
Text("The label panel grows up to 240 points.")
.font(.caption)
}
.padding(20)
.frame(
maxWidth: .infinity,
maxHeight: .infinity,
alignment: .topLeading
)
.frame(minWidth: 300, minHeight: 360)
}
}
Das Programm wurde geprüft, hier jedoch nicht kompiliert oder ausgeführt, da in dieser Umgebung weder ein Swift-Compiler noch eine macOS-Laufzeitumgebung verfügbar ist. Erstellen Sie es in Xcode und führen Sie die folgenden Prüfungen durch, bevor Sie es in eine bestehende Oberfläche integrieren.
Warum die Messung innerhalb der Scroll-View erfolgen muss
Der Geometrie-Callback erfasst den inneren Stack einschließlich Innenabständen. Würden Sie die äußere Scroll-View messen, erhielten Sie den Anzeigebereich, dessen Höhe Sie bereits festgelegt haben. Dadurch entstünde eine zirkuläre Größenregel, statt die erforderliche Inhaltshöhe zu ermitteln.
Die Transformation gibt nur die gerundete, begrenzte Höhe zurück. Apple empfiehlt, Geometrie in den benötigten Wert umzuwandeln und bei häufigen Geometrieänderungen umfangreiche Aktualisierungen zu vermeiden. Hier sind Änderungen der Scroll-Position für die Berechnung unerheblich, und Inhalte, die höher als der Grenzwert sind, ergeben weiterhin denselben Wert von 240 Punkten. Apples Hinweise zu Geometrieänderungen.
Durch Aufrunden wird vermieden, dass der Anzeigebereich absichtlich einen Bruchteil niedriger als der gemessene Inhalt festgelegt wird. Der anfängliche Wert von 240 Punkten stellt einen nutzbaren Anzeigebereich bereit, bevor die Messung vorliegt. Er dient als Startwert und entspricht nicht einer gemessenen Inhaltshöhe. Der Callback ersetzt ihn anschließend.
Warum Breite und Reihenfolge der Modifikatoren wichtig sind
Halten Sie die horizontale Größe flexibel. Das Beispiel verwendet .fixedSize(horizontal: false, vertical: true), damit die Beschriftungen die verfügbare Breite nutzen und zugleich ihre ideale vertikale Größe behalten können. Apple warnt davor, dass das Fixieren einer idealen Dimension dazu führen kann, dass eine Ansicht die Grenzen ihres übergeordneten Elements überschreitet. Beide Dimensionen zu fixieren ist daher keine geeignete Standardeinstellung für umbrechende Beschriftungen in einem schmalen Panel. Apples Dokumentation zu fixedSize(horizontal:vertical:).
Die Innenabstände von 12 Punkten kommen vor dem Messungsmodifikator und fließen deshalb in die gemessene Inhaltshöhe ein. Der Rahmen folgt auf die Höhenzuweisung für die äußere Ansicht und markiert die Begrenzung des Anzeigebereichs. So lässt sich leichter erkennen, ob ungenutzter Platz zum scrollbaren Panel oder zum umgebenden Fenster gehört.
Kurze Inhalte, Zeilenumbrüche und Verkleinerung überprüfen
Prüfen Sie das Verhalten im Beispiel mit der folgenden Reihenfolge und anschließend erneut, nachdem Sie das Panel in den tatsächlichen Container Ihrer App eingefügt haben:
- Starten Sie die App mit deaktiviertem Schalter
Show more labels. Der Rahmen sollte die beiden Beschriftungen und ihre Innenabstände umschließen; die Bildunterschrift sollte direkt darunter stehen. - Aktivieren Sie
Show more labels. Das Panel sollte bis zum Grenzwert von 240 Punkten wachsen. Scrollen Sie, um zu prüfen, ob die letzte Beschriftung erreichbar bleibt. - Verkleinern und vergrößern Sie das Fenster. Umgebrochene Beschriftungen sollten ihre Höhe ändern, ohne dass ihre letzten Zeilen verloren gehen.
- Deaktivieren Sie den Schalter wieder. Das Panel sollte sich auf die Höhe der kurzen Inhalte verkleinern.
- Ändern Sie das kurze Array vorübergehend in
[]. Die explizite MeldungNo labels to displaysollte die Inhaltshöhe bestimmen. - Wiederholen Sie den Test mit den Textstilen und umgebenden Steuerelementen Ihres tatsächlichen Panels.
Der Zustand für leere Inhalte ist eine bewusste Entscheidung für die Benutzeroberfläche: In diesem Beispiel bleiben bei einem leeren Array eine Meldung und die Innenabstände erhalten. Soll das Panel vollständig verschwinden, lassen Sie die gesamte Scroll-View bedingt weg, wenn keine Beschriftungen vorhanden sind. Das ist eine separate Produktentscheidung und betrifft nicht die Messung eines nicht leeren Panels.
Das umgebende Beispielfenster behält seine Mindesthöhe. Der Code passt die Höhe des Beschriftungsbereichs an; er fordert nicht, dass das Mac-Fenster schrumpft, wenn Beschriftungen entfernt werden. Der Platz unter der Bildunterschrift gehört zum äußeren Layout und liegt außerhalb der umrandeten Scroll-View.
Verbleibende Abstände oder abgeschnittene Beschriftungen beheben
Bleibt das Panel hoch, prüfen Sie zuerst, an welcher Stelle der Messungsmodifikator angebracht ist. Er sollte den Inhaltsstack einschließlich seiner internen Innenabstände erfassen. Prüfen Sie anschließend die übergeordneten Ansichten auf eine explizite Höhe oder Mindesthöhe, die Platz um das Panel reserviert. Apple dokumentiert, dass eine angegebene Rahmenhöhe eine feste Dimension ist. Ein zusätzlicher äußerer Rahmen kann daher Platz beibehalten, selbst wenn der innere Anzeigebereich schrumpft. SwiftUI-Rahmengrößen.
Bleibt der Inhalt selbst unerwartet hoch, entfernen Sie vertikale Größenexpansion aus dem gemessenen Teilbaum. Vermeiden Sie bei diesem Muster mit wenigen Beschriftungen einen vertikalen Spacer oder einen Inhaltsrahmen, dessen Mindesthöhe vom Anzeigebereich abhängt. Dadurch würde die Inhaltshöhe vom Anzeigebereich abhängen und die unabhängige Messung beeinträchtigen, die die Größenregel benötigt.
Falls der untere Innenabstand abgeschnitten wird, prüfen Sie, ob er vor der Messung einbezogen wird. Wenn Text nach der Integration anders umbricht, überprüfen Sie die tatsächliche Breite, die der neue übergeordnete Container bereitstellt. Eine bei einer bestimmten Breite gemessene Höhe sollte nicht als dauerhafte Höhe bei einer anderen Breite behandelt werden.
Passen Sie den Grenzwert von 240 Punkten an die umgebende Oberfläche an. Das Beispiel stellt eine Fenster-Mindesthöhe bereit, die für Steuerelemente, Innenabstände, Bildunterschrift und begrenztes Panel ausreicht. Ein niedrigerer Container benötigt ein kleineres Platzbudget oder ein anderes umgebendes Layout. Dieser feste Grenzwert berechnet nicht automatisch den Platz, der unter anderen Steuerelementen verbleibt.
Verwenden Sie diesen Ansatz für eine überschaubare Anzahl von Beschriftungen, die vollständig ausgelegt werden. Apple beschreibt LazyVStack so, dass Elemente erst bei Bedarf für die Darstellung auf dem Bildschirm erstellt werden. Gehen Sie daher nicht davon aus, dass der Ersatz des inneren Stacks durch einen Lazy-Stack dieselbe Messung des vollständigen Inhalts ermöglicht. Für eine große Sammlung ist in der Regel ein stabiler Anzeigebereich mit Lazy-Inhalt besser geeignet. Apples Dokumentation zu LazyVStack.
Wann eignen sich Messungen mit AppKit und TextKit?
Messen Sie für dieses SwiftUI-Beschriftungsfeld den dargestellten SwiftUI-Teilbaum. Eine AppKit-Textmessung würde eine weitere Layoutkonfiguration einführen, die mit den angezeigten Beschriftungen übereinstimmen müsste.
Wenn Ihre App bereits eine NSScrollView verwendet, behalten Sie dieselbe Unterscheidung zwischen Dokument und Anzeigebereich bei. Apple definiert documentView als die gescrollte Ansicht, während contentSize die Größe der Inhaltsansicht der Scroll-View bezeichnet. Trotz des Namens misst contentSize nicht das vollständige Dokument. AppKit documentView, AppKit contentSize.
Bei einem bestehenden TextKit-Layout auf Grundlage von NSLayoutManager gibt usedRect(for:) das Begrenzungsrechteck der Glyphen zurück, die bereits in einem Textcontainer angeordnet wurden. Apple weist ausdrücklich darauf hin, dass eine Abfrage weder Glyphen erzeugt noch ein Layout ausführt. Wenn ein vollständiges Container-Layout benötigt wird, ist ensureLayout(for:) die dokumentierte Methode, um dieses Layout zuerst zu erzwingen. TextKit usedRect(for:), TextKit ensureLayout(for:).
Auch dann entspricht das Glyphenrechteck nicht dem vollständigen Textrahmen. Apple dokumentiert zusätzlichen Platz um den Textcontainer durch textContainerInset sowie einen Innenabstand für Zeilenfragmente innerhalb des Containers. Berücksichtigen Sie die tatsächliche Konfiguration der Textansicht, statt eine Glyphenhöhe direkt für einen Scroll-Anzeigebereich zu übernehmen. Diese Methoden beschreiben den NSLayoutManager-Ansatz und sind keine allgemeine Größenlösung für jede Textkomponente. AppKit textContainerInset.
Für ein kleines SwiftUI-Beschriftungsfeld besteht die praktische Lösung darin, die Breite flexibel zu halten, den inneren Inhalt einschließlich Innenabständen zu messen und die Höhe des äußeren Anzeigebereichs zu begrenzen. Prüfen Sie das Ergebnis sowohl mit kurzen als auch mit langen Inhalten und testen Sie es anschließend im tatsächlichen übergeordneten Layout, damit die umgebenden Einschränkungen dasselbe kompakte Verhalten ermöglichen.
Quellen
- ScrollView — Apple
- min(::) — Apple
- frame(width:height:alignment:) — Apple
- scrollBounceBehavior(_:axes:) — Apple
- fixedSize(horizontal:vertical:) — Apple
- onGeometryChange(for:of:action:) — Apple
- LazyVStack — Apple
- documentView — Apple
- contentSize — Apple
- usedRect(for:) — Apple
- ensureLayout(for:) — Apple
- textContainerInset — Apple