Um verschachtelte Elemente in deiner Mac-App animiert aufzuklappen, verwende DisclosureGroup mit einer animierten Aufklapp-Bindung. Wenn du die Höhe beim Aufklappen präzise steuern musst, verwende ein benutzerdefiniertes Layout in einer Scroll-Ansicht; ein Inhaltsübergang allein legt diese Geometrie nicht fest.
Dieses Tutorial behandelt eine konkrete Aufgabe: eine kleine Hierarchie von Elementen aufzuklappen, sodass deren Inhalte eingeblendet werden und die darunterliegenden Elemente sich mit der veränderten Höhe verschieben. Die Beispiele verwenden synthetische Beschriftungen und laufen vollständig im eigenen Fenster der App.
Warum werden die Inhalte eingeblendet, ohne dass sie sichtbar aufklappen?
Unterscheide bei der Fehlersuche drei Aspekte: den Aufklappstatus, das Erscheinungsbild eingefügter Inhalte und den vom Container bereitgestellten Platz. Apple dokumentiert withAnimation, wertabhängige Animationsmodifikatoren und animierte Bindungen als Möglichkeiten, Zustandsänderungen zu animieren. Übergänge werden separat als Effekte dokumentiert, die beim Einfügen oder Entfernen von Ansichten aus der Hierarchie zum Einsatz kommen. Animationen
Ein Opazitätsübergang ändert die Transparenz. Ein Bewegungsübergang ändert die Position der ein- oder ausgeblendeten Ansicht relativ zu einer Kante. Keine dieser APIs legt für sich allein fest, wie eine umgebende Liste die Zeilenhöhen interpolieren muss. Ein Bewegungsübergang kann daher das sichtbare Einblenden verändern, ohne das konkrete Layoutverhalten zu beheben, das du erreichen möchtest. move(edge:)
Betrachte diese Unterscheidung als Diagnosehilfe, nicht als Beweis für eine bestimmte interne Implementierung: Wenn die Details eingeblendet werden, während die folgende Zeile springt, untersuche die Geometrie des Containers getrennt vom Übergang der Details. Die dokumentierte List-API beschreibt einen Zeilencontainer mit optionaler Auswahl; sie garantiert nicht für jede verschachtelte Anordnung und jeden Listenstil eine bestimmte Aufklappanimation. List
Beginne mit nativen verschachtelten Disclosure Groups
Für eine herkömmliche Mac-Hierarchie solltest du DisclosureGroup verwenden, bevor du ein eigenes Aufklappelement implementierst. Der Aufklappstatus wird über eine boolesche Bindung bereitgestellt, und Apples Dokumentation zeigt ausdrücklich, wie eine Disclosure Group in eine andere verschachtelt wird. Die API ist ab macOS 11 verfügbar. DisclosureGroup
Erstelle in Xcode ein macOS-App-Projekt mit SwiftUI. Ersetze die generierte App-Deklaration durch das folgende vollständige Programm und verwende im Target genau eine @main-Deklaration. Es enthält sämtliche Beispielinhalte und beide Aufklappzustände.
import SwiftUI
@main
struct NestedDisclosureDemoApp: App {
var body: some Scene {
WindowGroup {
NativeHierarchyView()
.frame(minWidth: 360, minHeight: 280)
}
}
}
struct NativeHierarchyView: View {
@Environment(\.accessibilityReduceMotion) private var reduceMotion
@State private var collectionExpanded = false
@State private var shapesExpanded = false
private var expansionAnimation: Animation? {
reduceMotion ? nil : .easeInOut(duration: 0.25)
}
var body: some View {
List {
DisclosureGroup(
"Collection",
isExpanded: $collectionExpanded.animation(expansionAnimation)
) {
DisclosureGroup(
"Shapes",
isExpanded: $shapesExpanded.animation(expansionAnimation)
) {
Text("Circle")
Text("Square")
}
Text("Palette")
}
Text("Following item")
}
}
}
Die Methode animation der Bindung fordert eine Animation an, wenn sich ihr Wert ändert. Klicke auf Collection, klappe dann Shapes auf und anschließend Collection zu und wieder auf. Durch den separat gespeicherten Zustand merkt sich das Beispiel, ob Shapes aufgeklappt war, während die übergeordnete Gruppe geschlossen war. Dieser Zustand bleibt im Speicher der aktuellen Ansicht und wird nicht über App-Starts hinweg gespeichert. Animierte Bindungen gehören zu Apples dokumentierten Möglichkeiten, Animationen auszulösen. Animationen
Das Beispiel verwendet zur Veranschaulichung eine viertelsekündige Ease-Kurve. Wenn die Systemeinstellung Reduce Motion aktiviert ist, übergibt es nil. Apple stellt diese Einstellung über accessibilityReduceMotion bereit und rät davon ab, große Animationen zu verwenden, wenn sie aktiviert ist. accessibilityReduceMotion
Prüfe die native Version unter jeder unterstützten macOS-Version und in jedem von deiner App unterstützten Listenstil. Wenn sie sich wie gewünscht verhält, behalte sie bei. Wenn nur das Erscheinungsbild des Aufklappbereichs angepasst werden muss, bietet DisclosureGroupStyle ab macOS 13 einen unterstützten Anpassungspunkt. Ein benutzerdefinierter Stil setzt weiterhin Ansichten zusammen; seine Dokumentation garantiert keine beliebige Kontrolle über die Zeilenanimation der umgebenden Liste. DisclosureGroupStyle
Wähle den Container passend zum gewünschten Ergebnis
Nutze diese Entscheidungshilfe, bevor du weitere Modifikatoren hinzufügst. Sie unterscheidet eine native Interaktion mit Zeilen von einem explizit gesteuerten Einblenden.
| Gewünschtes Ergebnis | Ausgangspunkt | Was zu prüfen ist |
|---|---|---|
| Herkömmliche aufklappbare Zeilen mit Listenauswahl | List und DisclosureGroup |
Aufklappverhalten unter der tatsächlichen macOS-Version und im tatsächlichen Listenstil |
| Anderes Erscheinungsbild der Aufklappbeschriftung oder des Steuerelements | DisclosureGroupStyle |
Ob der umgebende Container weiterhin die gewünschte Geometrie erzeugt |
| Genau festgelegte Einblendehöhe für eine kleine Hierarchie | ScrollView, VStack und ein animierbares Layout |
Verschachtelte Größenänderungen, Clipping und Verhalten der Steuerelemente |
| Vorhandene AppKit-Gliederungsansicht mit Spalten und Delegaten | NSOutlineView |
Die zugehörigen Aufklapp-APIs und das Verhalten der Datenquelle |
Der benutzerdefinierte Ansatz weiter unten verwendet bewusst einen Stack in einer Scroll-Ansicht. Er stellt eigene Aufklappschaltflächen und Geometrie bereit. Daher musst du auch jede von deiner Oberfläche benötigte Listenauswahl oder Zeilenaktion implementieren. Apple dokumentiert Auswahl als Funktion von List; ein Wechsel des Containers ändert diesen Ausgangspunkt. List
Animiere die eingeblendete Höhe mit einem benutzerdefinierten Layout
Ab macOS 13 ermöglicht das Protokoll Layout einem Container, seine Größe zu berechnen und seine untergeordneten Ansichten zu platzieren. Es übernimmt außerdem das Protokoll Animatable. Das sind die dokumentierten Bausteine, um den Aufklappgrad als interpolierten Wert umzusetzen, statt sich allein auf Einblendeffekte zu verlassen. Layout
Die folgende eigenständige Implementierung verwendet für jeden Einblendbereich einen untergeordneten Stack. Sie misst diesen Stack bei der verfügbaren Breite und einer nicht vorgegebenen Höhe und gibt dann einen Bruchteil der gemessenen Höhe zurück. Der untergeordnete Stack bleibt oben positioniert, während sich der sichtbare Bereich verändert. Apple dokumentiert sowohl das Messen einer Unteransicht mit einem Vorschlag als auch das Platzieren mit einem passenden Vorschlag. sizeThatFits(_:), place(at:anchor:proposal:)
Verwende dieses Beispiel als alternatives vollständiges Programm und ersetze damit das vorherige. Lege das Bereitstellungsziel auf macOS 13 oder neuer fest. Der Code wurde anhand der dokumentierten APIs geprüft; er wurde hier nicht kompiliert oder visuell getestet, da diese Umgebung weder über eine Swift-Programmdatei noch eine macOS-Laufzeitumgebung verfügt.
import SwiftUI
@main
struct RevealDemoApp: App {
var body: some Scene {
WindowGroup {
ScrollView {
VStack(alignment: .leading, spacing: 0) {
RevealGroup("Collection") {
RevealGroup("Shapes") {
Text("Circle")
Text("Square")
}
Text("Palette")
}
Text("Following item")
.padding(.vertical, 8)
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding()
}
.frame(minWidth: 360, minHeight: 280)
}
}
}
struct RevealGroup<Content: View>: View {
let title: String
let content: Content
@State private var expanded = false
@Environment(\.accessibilityReduceMotion) private var reduceMotion
init(_ title: String, @ViewBuilder content: () -> Content) {
self.title = title
self.content = content()
}
var body: some View {
VStack(alignment: .leading, spacing: 0) {
Button {
withAnimation(
reduceMotion ? nil : .easeInOut(duration: 0.25)
) {
expanded.toggle()
}
} label: {
HStack {
Image(systemName: "chevron.right")
.rotationEffect(.degrees(expanded ? 90 : 0))
.accessibilityHidden(true)
Text(title)
Spacer(minLength: 0)
}
.padding(.vertical, 6)
.contentShape(Rectangle())
}
.buttonStyle(.plain)
.accessibilityValue(expanded ? "Expanded" : "Collapsed")
RevealLayout(progress: expanded ? 1 : 0) {
VStack(alignment: .leading, spacing: 8) {
content
}
.padding(.leading, 18)
.padding(.bottom, 8)
.frame(maxWidth: .infinity, alignment: .leading)
}
.clipped()
.allowsHitTesting(expanded)
.disabled(!expanded)
.accessibilityHidden(!expanded)
}
}
}
struct RevealLayout: Layout {
var progress: CGFloat
var animatableData: CGFloat {
get { progress }
set { progress = newValue }
}
func sizeThatFits(
proposal: ProposedViewSize,
subviews: Subviews,
cache: inout ()
) -> CGSize {
guard let child = subviews.first else { return .zero }
let width = proposal.width.flatMap { $0.isFinite ? $0 : nil }
let size = child.sizeThatFits(
ProposedViewSize(width: width, height: nil)
)
return CGSize(
width: width ?? size.width,
height: size.height * min(max(progress, 0), 1)
)
}
func placeSubviews(
in bounds: CGRect,
proposal: ProposedViewSize,
subviews: Subviews,
cache: inout ()
) {
guard let child = subviews.first else { return }
child.place(
at: CGPoint(x: bounds.minX, y: bounds.minY),
anchor: .topLeading,
proposal: ProposedViewSize(width: bounds.width, height: nil)
)
}
}
Der entscheidende Wert ist progress. SwiftUI interpoliert während einer Animation die alten und neuen animatableData-Werte. Dadurch ändert sich hier die zurückgegebene Höhe kontinuierlich von null bis zur gemessenen Höhe der untergeordneten Ansicht. Beträgt die gemessene Höhe beispielsweise 160 Punkte, ergibt sich bei einem Fortschritt von 0,5 ein 80 Punkte hoher Einblendbereich. Diese Zahlen dienen nur als Rechenbeispiel und sind keine Messwerte aus einer App. Animatable
Die nicht vorgegebene Höhe fordert die ideale Höhe des untergeordneten Inhalts bei der vorgeschlagenen Breite an, statt ihn auf die aktuelle Einblendehöhe zu komprimieren. Das ist bei umbrochenen Beschriftungen wichtig: Der Einblendbereich soll deren Layout sichtbar machen, nicht es schrittweise zusammendrücken. Apple beschreibt nicht vorgegebene Abmessungen als Möglichkeit, die ideale Größe zu ermitteln. ProposedViewSize
Durch Clipping wird anschließend der Teil außerhalb der zurückgegebenen Begrenzungen ausgeblendet. Ohne Clipping kann der Inhalt über den vom Layout reservierten Bereich hinaus gezeichnet werden. Belasse den unteren Abstand innerhalb des Einblendbereichs wie gezeigt, damit er in die Höhe eingeht, die beim Zuklappen verschwindet. clipped(antialiased:)
Diese Implementierung behält die Hierarchie der untergeordneten Ansichten beim Zuklappen bewusst bei. Sie deaktiviert Interaktionen und entfernt den zugeklappten Bereich aus der Barrierefreiheitshierarchie; die sichtbare Schaltfläche gibt ihren Aufklappstatus an. Trefferauswertung und Sichtbarkeit für die Barrierefreiheit sind getrennte Einstellungen, daher reicht Clipping allein nicht aus. allowsHitTesting(_:), accessibilityHidden(_:)
Prüfe das Verhalten verschachtelter Gruppen, bevor du die benutzerdefinierte Version verwendest
Mit der folgenden Abfolge kannst du feststellen, ob das Einblenden funktioniert oder nur im einfachsten Zustand gut aussieht.
- Klappe
Collectionund dannShapesauf. BeobachteFollowing item, während sich beide Ebenen verändern. - Klappe
Collectionzu, währendShapesgeöffnet ist, und öffne sie wieder. Prüfe, ob der gewünschte Aufklappzustand der untergeordneten Gruppe erhalten bleibt. - Klicke erneut auf eine Aufklappschaltfläche, bevor ihre Animation abgeschlossen ist. Prüfe, ob sich das Einblenden umkehrt, ohne eine Lücke zu hinterlassen.
- Verkleinere das Fenster und ersetze eine Beispielbeschriftung durch eine längere synthetische Beschriftung. Prüfe den Textumbruch und die endgültige Höhe des Einblendbereichs.
- Aktiviere
Reduce Motionund prüfe, ob das Aufklappen auch ohne animierte Bewegung funktioniert. - Prüfe die Aktivierung per Tastatur und die Darstellung der aufgeklappten und zugeklappten Gruppen in der Barrierefreiheitshierarchie.
Das benutzerdefinierte Layout ist für Inhalte mit begrenzter Höhe wie diese Beschriftungen und verschachtelten Stacks gedacht. Platziere darin keine vertikal unbegrenzte Scroll-Ansicht in der Erwartung, dass sie eine brauchbare Gesamthöhe des Inhalts liefert. Die Implementierung fordert ausdrücklich die ideale Höhe an, daher muss der untergeordnete Inhalt auf diesen Vorschlag eine sinnvolle Antwort geben können. Das folgt aus der gewählten Größenberechnung. ProposedViewSize
Auch zugeklappte Inhalte verursachen weiterhin Erstellungs- und Layoutkosten. Beginne bei einer kleinen Hierarchie mit einem einfachen VStack; prüfe eine andere Architektur, bevor du dieses eager Beispiel auf einen großen Baum überträgst. Achte bei datenbasierten Zeilen darauf, dass Modellkennungen beim Aufklappen und Umordnen stabil bleiben. Das Swift-Protokoll Identifiable repräsentiert die stabile Identität eines Objekts, und ForEach verwendet identifizierte Daten, um Ansichten zu erzeugen. Identifiable, ForEach
Welche Rolle spielen AppKit und TextKit?
Wenn die Oberfläche bereits AppKit verwendet, ist NSOutlineView das dokumentierte Steuerelement für hierarchische Zeilen und Spalten. Seine Methode expandItem(_:) klappt ein Element auf und hat keine Wirkung, wenn es bereits geöffnet ist oder nicht aufgeklappt werden kann. Diese Methode bietet keinen Parameter für eine SwiftUI-Animationskurve. Gehe daher nicht davon aus, dass sie das oben gezeigte benutzerdefinierte Timing ermöglicht. NSOutlineView, expandItem(_:)
TextKit verwaltet Textspeicherung, Textlayout und Darstellung, einschließlich der Komponenten, die von NSTextView verwendet werden. Bei einer Hierarchie gewöhnlicher Beschriftungen ist es daher eine separate Angelegenheit vom aufklappbaren Container. Verwende es, wenn die Inhalte selbst Textbearbeitung oder ein benutzerdefiniertes Textlayout benötigen; es ist kein dokumentierter Schalter zum Animieren des Aufklappens einer SwiftUI-Liste. TextKit
Beginne mit nativen Disclosure Groups und prüfe ihr Verhalten in der Mac-Oberfläche, für die du entwickelst. Wenn die Aufgabe eine kontrollierte Höhenanimation erfordert, gibt das benutzerdefinierte Layout dieser Höhe einen explizit animierbaren Wert. Richte die Wahl nach der benötigten Interaktion aus: natives Listenverhalten, benutzerdefinierte Geometrie des Einblendbereichs oder eine bestehende AppKit-Gliederungsansicht.
Quellen
- DisclosureGroup — Apple
- Animationen — Apple
- move(edge:) — Apple
- List — Apple
- accessibilityReduceMotion — Apple
- DisclosureGroupStyle — Apple
- Layout — Apple
- sizeThatFits(_:) — Apple
- place(at:anchor:proposal:) — Apple
- Animatable — Apple
- ProposedViewSize — Apple
- clipped(antialiased:) — Apple
- allowsHitTesting(_:) — Apple
- accessibilityHidden(_:) — Apple
- Identifiable — Apple
- ForEach — Apple
- NSOutlineView — Apple
- expandItem(_:) — Apple
- TextKit — Apple