Pour animer le déploiement imbriqué dans votre app Mac, commencez avec DisclosureGroup et une liaison de déploiement animée. Si vous avez besoin d’un contrôle précis sur la hauteur de déploiement, utilisez une disposition personnalisée dans une vue de défilement ; une transition de contenu à elle seule ne définit pas cette géométrie.
Ce tutoriel porte sur une tâche précise : déployer une petite hiérarchie d’éléments afin d’en révéler le contenu et de déplacer les éléments situés en dessous en fonction de la variation de hauteur. Les exemples utilisent des libellés synthétiques et s’exécutent entièrement dans la fenêtre de l’app.
Pourquoi le contenu s’estompe-t-il sans que le déploiement soit visible ?
Pour diagnostiquer le résultat, distinguez trois éléments : l’état de déploiement, l’apparence du contenu inséré et l’espace alloué par le conteneur. Apple présente withAnimation, les modificateurs d’animation pilotés par une valeur et les liaisons animées comme des moyens d’animer les changements d’état. La documentation décrit séparément les transitions comme les effets utilisés lorsque des vues entrent dans la hiérarchie ou en sortent. Animations
Une transition d’opacité modifie la transparence. Une transition de déplacement modifie la position de la vue qui entre ou sort par rapport à un bord. Aucune de ces API ne précise à elle seule comment une liste environnante doit interpoler la hauteur de ses lignes. L’ajout d’une transition de déplacement peut donc modifier l’apparition visible sans régler le comportement de disposition particulier que vous recherchez. move(edge:)
Considérez cette distinction comme une piste de diagnostic, et non comme la preuve d’une implémentation interne particulière : si les détails s’estompent tandis que la ligne suivante saute, examinez séparément la géométrie du conteneur et la transition des détails. La documentation de l’API List décrit un conteneur de lignes avec sélection facultative ; elle ne garantit pas une courbe de déploiement particulière pour chaque disposition imbriquée et chaque style de liste. List
Commencez avec des groupes de divulgation imbriqués natifs
Pour une hiérarchie Mac classique, utilisez DisclosureGroup avant d’implémenter votre propre contrôle de divulgation. Il expose le déploiement via une liaison booléenne, et la documentation d’Apple montre explicitement un groupe de divulgation imbriqué dans un autre. L’API est disponible sous macOS 11 et versions ultérieures. DisclosureGroup
Créez dans Xcode un projet d’app macOS utilisant SwiftUI. Remplacez la déclaration d’app générée par le programme complet suivant, en conservant exactement une déclaration @main dans la cible. Il comprend tout le contenu d’exemple et les deux états de déploiement.
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")
}
}
}
La méthode animation de la liaison demande une animation lorsque sa valeur change. Cliquez sur Collection, déployez Shapes, puis repliez et rouvrez Collection. L’état stocké indépendamment permet à l’exemple de mémoriser si Shapes était déployé lorsque son parent était fermé. Cet état reste en mémoire pour la vue actuelle ; il n’est pas conservé d’un lancement de l’app à l’autre. Les liaisons animées font partie des points d’entrée à l’animation documentés par Apple. Animations
L’exemple utilise une courbe d’accélération d’un quart de seconde à titre de choix de conception. Il transmet nil lorsque la préférence système Réduire les animations est activée. Apple expose cette préférence via accessibilityReduceMotion et conseille d’éviter les animations importantes lorsqu’elle est activée. accessibilityReduceMotion
Évaluez la version native sur chaque version de macOS et pour chaque style de liste pris en charge par votre app. Si son comportement convient à la tâche, gardez-la. Si seule l’apparence du contrôle de divulgation doit être modifiée, DisclosureGroupStyle fournit un point de personnalisation pris en charge sous macOS 13 et versions ultérieures. Un style personnalisé compose toujours des vues ; sa documentation ne garantit pas un contrôle arbitraire de l’animation des lignes de la liste environnante. DisclosureGroupStyle
Choisissez le conteneur en fonction du résultat recherché
Utilisez cette aide à la décision avant d’ajouter d’autres modificateurs. Elle distingue une interaction native sur des lignes d’une révélation dont vous contrôlez explicitement le comportement.
| Résultat recherché | Point de départ | Élément à vérifier |
|---|---|---|
| Lignes déployables classiques avec sélection dans la liste | List et DisclosureGroup |
Comportement du déploiement avec la version de macOS et le style de liste utilisés |
| Apparence différente du libellé ou du contrôle de divulgation | DisclosureGroupStyle |
Capacité du conteneur environnant à produire la géométrie souhaitée |
| Hauteur de révélation précisément définie pour une petite hiérarchie | ScrollView, VStack et une disposition animable |
Redimensionnement imbriqué, découpage et comportement des contrôles |
| Vue d’ensemble AppKit existante avec colonnes et délégués | NSOutlineView |
Ses propres API de déploiement et le comportement de sa source de données |
L’approche personnalisée ci-dessous utilise délibérément une pile dans une vue de défilement. Elle fournit ses propres boutons de divulgation et sa propre géométrie ; vous devez donc aussi implémenter toute sélection dans la liste ou toute commande de ligne requise par votre interface. Apple documente la sélection comme une fonctionnalité de List ; changer de conteneur modifie ce point de départ. List
Animez la hauteur révélée avec une disposition personnalisée
Sous macOS 13 et versions ultérieures, le protocole Layout permet à un conteneur de calculer sa taille et de positionner ses enfants. Il hérite aussi du protocole Animatable. Ces éléments documentés permettent de faire interpoler la valeur de déploiement au lieu de compter uniquement sur les effets d’insertion. Layout
L’implémentation originale suivante utilise une pile enfant par révélation. Elle mesure cette pile à la largeur disponible avec une hauteur non spécifiée, puis renvoie une fraction de la hauteur mesurée. L’enfant reste positionné en haut tandis que la région visible change. Apple documente la mesure d’une sous-vue à l’aide d’une proposition ainsi que son positionnement avec une proposition correspondante. sizeThatFits(_:), place(at:anchor:proposal:)
Utilisez ceci comme programme complet de remplacement, à la place de l’exemple précédent. Définissez la cible de déploiement sur macOS 13 ou une version ultérieure. Le code a été vérifié par rapport aux API documentées ; il n’a pas été compilé ni testé visuellement ici, car cet environnement ne dispose ni d’un exécutable Swift ni d’un environnement d’exécution macOS.
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)
)
}
}
La valeur essentielle est progress. SwiftUI interpole les anciennes et nouvelles valeurs animatableData pendant une animation. Ici, cela fait varier progressivement la hauteur renvoyée entre zéro et la hauteur mesurée de l’enfant. À titre d’illustration, une hauteur mesurée de 160 points produit une révélation de 80 points à la progression 0,5 ; ces nombres sont des exemples arithmétiques, pas des résultats mesurés dans l’app. Animatable
La hauteur non spécifiée demande à l’enfant sa hauteur idéale pour la largeur proposée, plutôt que de lui demander de se comprimer dans la hauteur de révélation actuelle. C’est important pour les libellés qui passent à la ligne : la révélation doit montrer leur disposition, sans les comprimer progressivement. Apple décrit les dimensions non spécifiées comme un moyen d’obtenir des dimensions idéales. ProposedViewSize
Le découpage masque ensuite la partie située en dehors des limites renvoyées. Sans lui, le contenu peut être dessiné au-delà de la région réservée par la disposition. Conservez le remplissage inférieur à l’intérieur de la révélation, comme indiqué, afin qu’il contribue à la hauteur qui sera réduite. clipped(antialiased:)
Cette implémentation conserve volontairement la hiérarchie enfant lorsqu’elle est repliée. Elle désactive les interactions et retire la région repliée de la représentation d’accessibilité ; le bouton visible indique son état de déploiement. Le test des interactions et la visibilité pour l’accessibilité sont des contrôles distincts : le découpage seul ne suffit pas. allowsHitTesting(_:), accessibilityHidden(_:)
Vérifiez le comportement imbriqué avant d’adopter la version personnalisée
Suivez cette séquence pour distinguer une révélation fonctionnelle d’une animation qui ne semble correcte que dans l’état le plus simple.
- Déployez
Collection, puisShapes. ObservezFollowing itempendant que les deux niveaux changent. - Repliez
Collectionalors queShapesest ouvert, puis rouvrez-la. Vérifiez que l’état de déploiement enfant souhaité est conservé. - Cliquez de nouveau sur un bouton de divulgation avant la fin de son animation. Vérifiez que la révélation s’inverse sans laisser d’espace vide.
- Réduisez la largeur de la fenêtre et remplacez un libellé d’exemple par un libellé synthétique plus long. Vérifiez le retour à la ligne et la hauteur finale révélée.
- Activez
Réduire les animationset vérifiez que le déploiement reste utilisable sans déplacement animé. - Vérifiez l’activation au clavier et la représentation d’accessibilité des groupes déployés et repliés.
La disposition personnalisée est conçue pour du contenu de hauteur finie, comme ces libellés et ces piles imbriquées. Évitez de placer une zone de défilement verticalement non bornée dans la révélation en espérant qu’elle fournisse une hauteur de contenu totale utile. L’implémentation demande explicitement une hauteur idéale ; l’enfant doit donc pouvoir répondre de manière pertinente à cette proposition. C’est une conséquence du choix de dimensionnement. ProposedViewSize
La conservation du contenu replié conserve aussi ses coûts de construction et de disposition. Pour une petite hiérarchie, commencez par un VStack ordinaire ; évaluez une autre architecture avant d’appliquer cet exemple à une grande arborescence. Pour des lignes générées à partir de données, conservez des identifiants de modèle stables lors des déploiements et des réorganisations. Le protocole Swift Identifiable représente une identité stable pour les entités, et ForEach utilise des données identifiées pour produire des vues. Identifiable, ForEach
Quel est le rôle d’AppKit et de TextKit ?
Si l’interface utilise déjà AppKit, NSOutlineView est le contrôle documenté pour afficher des lignes et des colonnes hiérarchiques. Sa méthode expandItem(_:) déploie un élément et ne fait rien si celui-ci est déjà déployé ou ne peut pas l’être. Cette méthode n’expose pas de paramètre de courbe d’animation SwiftUI ; ne supposez donc pas qu’elle fournit le minutage personnalisé présenté plus haut. NSOutlineView, expandItem(_:)
TextKit gère le stockage, la disposition et le rendu du texte, y compris les mécanismes utilisés par NSTextView. Pour une hiérarchie de libellés ordinaires, il s’agit donc d’un sujet distinct du conteneur de déploiement. Utilisez-le lorsque le contenu nécessite l’édition de texte ou une disposition textuelle personnalisée ; ce n’est pas un réglage documenté pour animer le déploiement d’une liste SwiftUI. TextKit
Commencez par des groupes de divulgation natifs et vérifiez leur comportement dans l’interface Mac cible. Si la tâche exige une révélation dont la hauteur est contrôlée, la disposition personnalisée donne à cette hauteur une valeur animable explicite. Faites votre choix en fonction de l’interaction requise : comportement natif d’une liste, géométrie de révélation personnalisée ou vue d’ensemble AppKit existante.
Sources
- DisclosureGroup — Apple
- Animations — 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