Börja med DisclosureGroup och en animerad expansionsbindning för att animera expansion av nästlade vyer i din Mac-app. Om du behöver exakt kontroll över expansionshöjden använder du en anpassad layout i en rullningsvy; en innehållsövergång definierar inte den geometrin i sig.
Den här handledningen behandlar en specifik uppgift: att expandera en liten hierarki av objekt så att innehållet visas och objekten nedanför flyttas i takt med att höjden ändras. Exemplen använder syntetiska etiketter och körs helt i appens eget fönster.
Varför tonas innehållet in utan att expandera synligt?
Skilj på tre saker när du undersöker resultatet: expansionstillståndet, hur infogat innehåll visas och utrymmet som behållaren avsätter. Apple dokumenterar withAnimation, värdestyrda animeringsmodifierare och animerade bindningar som sätt att animera tillståndsändringar. Övergångar dokumenteras separat som effekter som används när vyer läggs till i eller tas bort från hierarkin. Animationer
En opacitetövergång ändrar genomskinligheten. En rörelseövergång ändrar den inkommande eller utgående vyns position i förhållande till en kant. Inget av dessa API:er anger i sig hur en omgivande lista måste interpolera radhöjderna. En rörelseövergång kan därför ändra hur vyn visas utan att lösa det specifika layoutbeteende du vill ha. move(edge:)
Se detta som en diagnostisk skillnad, inte som ett bevis på en viss intern implementering: om detaljerna tonas in medan raden nedanför hoppar, undersök behållarens geometri separat från detaljernas övergång. Apples dokumentation för List beskriver en radbehållare med valfri markering; den utlovar inte en viss expansionskurva för varje nästlad struktur och liststil. List
Börja med inbyggda nästlade DisclosureGroup-vyer
Använd DisclosureGroup för en konventionell Mac-hierarki innan du implementerar en egen kontroll för att visa och dölja innehåll. Den exponerar expansion via en boolesk bindning, och Apples dokumentation visar uttryckligen hur en DisclosureGroup kan placeras inuti en annan. API:et är tillgängligt i macOS 11 och senare. DisclosureGroup
Skapa ett macOS-approjekt i Xcode med SwiftUI. Ersätt den genererade appdeklarationen med följande fullständiga program och behåll exakt en @main-deklaration i målet. Det innehåller allt exempelmaterial och båda expansionstillstånden.
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")
}
}
}
Bindningens animation-metod begär animering när dess värde ändras. Klicka på Collection, expandera Shapes och dölj sedan och öppna Collection igen. Det separat lagrade tillståndet gör att exemplet kan komma ihåg om Shapes var expanderad medan den överordnade gruppen var stängd. Det här är tillstånd i minnet för den aktuella vyn, inte beständig lagring mellan appstarter. Animerade bindningar är en av Apples dokumenterade ingångar till animering. Animationer
Exemplet använder en kvarts sekund lång mjukningskurva som ett illustrativt designval. Det skickar nil när systeminställningen Reduce Motion är aktiverad. Apple tillhandahåller den inställningen via accessibilityReduceMotion och rekommenderar att stora animeringar undviks när den är aktiverad. accessibilityReduceMotion
Utvärdera den inbyggda versionen för varje macOS-version och liststil som appen stöder. Behåll den om beteendet uppfyller kraven. Om bara utseendet på hur innehåll visas och döljs behöver justeras erbjuder DisclosureGroupStyle en stödd anpassningspunkt i macOS 13 och senare. En anpassad stil bygger fortfarande ihop vyer; dokumentationen utlovar inte godtycklig kontroll över den omgivande listans radanimering. DisclosureGroupStyle
Välj behållare efter önskat resultat
Använd den här beslutshjälpen innan du lägger till fler modifierare. Den skiljer mellan en inbyggd radinteraktion och en uttryckligen styrd visning.
| Önskat resultat | Utgångspunkt | Vad du bör verifiera |
|---|---|---|
| Konventionella expanderbara rader med listmarkering | List och DisclosureGroup |
Expansionsbeteendet i den aktuella macOS-versionen och liststilen |
| Annat utseende på etiketten eller kontrollen för att visa och dölja innehåll | DisclosureGroupStyle |
Om den omgivande behållaren fortfarande ger önskad geometri |
| En exakt definierad visningshöjd för en liten hierarki | ScrollView, VStack och en animerbar Layout |
Nästlad storleksändring, beskärning och kontrollbeteende |
| En befintlig AppKit-översikt med kolumner och delegater | NSOutlineView |
Dess egna expansions-API:er och datakällans beteende |
Den anpassade lösningen nedan använder medvetet en stack i en rullningsvy. Den tillhandahåller egna knappar för att visa och dölja innehåll och egen geometri, så du behöver också implementera eventuell listmarkering eller radkommandon som gränssnittet kräver. Apple dokumenterar markering som en funktion i List; om du byter behållare ändras den utgångspunkten. List
Animera den visade höjden med en anpassad layout
I macOS 13 och senare låter protokollet Layout en behållare beräkna sin storlek och placera sina underordnade vyer. Det ärver också från Animatable. Det här är de dokumenterade byggstenarna för att göra expansionsmängden till ett interpolerat tal i stället för att enbart förlita sig på infogningseffekter. Layout
Följande ursprungliga implementering använder en underordnad stack per visning. Den mäter stacken vid tillgänglig bredd med ospecificerad höjd och rapporterar sedan en andel av den uppmätta höjden. Den underordnade vyn förblir placerad högst upp medan det synliga området ändras. Apple dokumenterar både hur en underordnad vy mäts med ett förslag och hur den placeras med ett motsvarande förslag. sizeThatFits(_:), place(at:anchor:proposal:)
Använd detta som ett alternativt fullständigt program och ersätt det föregående exemplet. Ställ in distributionsmålet på macOS 13 eller senare. Koden har granskats mot de dokumenterade API:erna; den har inte kompilerats eller testats visuellt här eftersom den här miljön saknar både en Swift-körbar fil och macOS-körmiljö.
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)
)
}
}
Det avgörande värdet är progress. SwiftUI interpolerar de gamla och nya värdena för animatableData under en animering. Här ändrar det den rapporterade höjden kontinuerligt från noll till den uppmätta höjden på den underordnade vyn. Som illustration ger en uppmätt höjd på 160 punkter en visning på 80 punkter vid förloppet 0,5; de värdena är räkneexempel, inte uppmätta appresultat. Animatable
Den ospecificerade höjden ber barnet om sin idealhöjd vid den föreslagna bredden, i stället för att be det krympa till den aktuella visningshöjden. Det här är viktigt för etiketter med radbrytning: visningen ska visa deras layout, inte gradvis pressa ihop den. Apple beskriver ospecificerade dimensioner som ett sätt att hämta ideal storlek. ProposedViewSize
Beskärningen döljer sedan den del som ligger utanför de rapporterade gränserna. Utan den kan innehåll ritas utanför det område som layouten reserverar. Behåll utfyllnaden längst ned inuti visningen, som i exemplet, så bidrar den till höjden som döljs. clipped(antialiased:)
Den här implementeringen behåller avsiktligt den underordnade hierarkin när den är dold. Den inaktiverar interaktion och tar bort det dolda området från hjälpmedelsrepresentationen; den synliga knappen anger expansionstillståndet. Träfftestning och synlighet för hjälpmedel är separata kontroller, så beskärning i sig räcker inte. allowsHitTesting(_:), accessibilityHidden(_:)
Kontrollera nästlat beteende innan du väljer den anpassade versionen
Använd följande sekvens för att skilja en fungerande visning från en animering som bara ser rätt ut i det enklaste tillståndet.
- Expandera
Collectionoch sedanShapes. Håll ögonen påFollowing itemmedan båda nivåerna ändras. - Dölj
CollectionmedanShapesär öppnad och öppna den sedan igen. Bekräfta att barnets expansionstillstånd bevaras som avsett. - Klicka på en knapp för att visa och dölja innehåll igen innan animeringen är klar. Kontrollera att visningen vänder utan att lämna ett mellanrum.
- Gör fönstret smalare och ersätt en exemp etikett med en längre syntetisk etikett. Kontrollera radbrytningen och den slutliga visningshöjden.
- Aktivera
Reduce Motionoch bekräfta att expansionen fortfarande går att använda utan animerad rörelse. - Kontrollera tangentbordsaktivering och hjälpmedelsrepresentationen för både expanderade och dolda grupper.
Den anpassade layouten är utformad för innehåll med begränsad höjd, som de här etiketterna och nästlade stackarna. Placera inte ett vertikalt obegränsat rullningsområde i visningen och förvänta dig att det ska ge en användbar fullständig innehållshöjd. Implementeringen begär uttryckligen en idealhöjd, så den underordnade vyn måste kunna svara meningsfullt på det förslaget. Det följer av den valda storleksdesignen. ProposedViewSize
Att behålla dolt innehåll innebär också att kostnaderna för att skapa och layouta det kvarstår. Börja med en vanlig VStack för en liten hierarki; utvärdera en annan arkitektur innan du använder det här ivriga exemplet för ett stort träd. För datastyrda rader ska modellernas identifierare förbli stabila vid expansion och omordning. Swifts protokoll Identifiable representerar en stabil entitetsidentitet, och ForEach använder identifierade data för att skapa vyer. Identifiable, ForEach
Vilken roll har AppKit och TextKit?
Om gränssnittet redan använder AppKit är NSOutlineView den dokumenterade kontrollen för hierarkiska rader och kolumner. Dess metod expandItem(_:) expanderar ett objekt och gör ingenting om objektet redan är expanderat eller inte kan expanderas. Metoden exponerar ingen parameter för animeringskurva i SwiftUI, så anta inte att den ger den anpassade tidsstyrning som visas ovan. NSOutlineView, expandItem(_:)
TextKit hanterar textlagring, textlayout och rendering, inklusive den teknik som används av NSTextView. För en hierarki av vanliga etiketter är det därför en separat fråga från behållaren som expanderas. Använd det när innehållet kräver textredigering eller anpassad textlayout; det är ingen dokumenterad omkopplare för att animera expansion av SwiftUI-listor. TextKit
Börja med inbyggda grupper för att visa och dölja innehåll och verifiera beteendet i Mac-gränssnittet du riktar dig mot. När uppgiften kräver en visning med kontrollerad höjd ger den anpassade layouten höjden ett uttryckligt animerbart värde. Låt valet styras av interaktionen du behöver: inbyggt listbeteende, anpassad visningsgeometri eller en befintlig AppKit-översikt.
Källor
- DisclosureGroup — Apple
- Animationer — 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