← Tous les articles

What SwiftUI Is Made Of

TITLE : De quoi est fait SwiftUI DESCRIPTION : SwiftUI est un DSL à result builder posé sur un arbre de View à types valeur. Une fois le substrat visible, AnyView, Group et ViewBuilder cessent d’être mystérieux. BODY: Genre : framework-explainer. Cet article explique le substrat sur lequel repose SwiftUI : result builders, types de retour opaques et arbre de vues à types valeur. Une fois le substrat visible, les éléments de SwiftUI qui surprennent les développeurs (AnyView, Group, ViewBuilder, les paramètres @ViewBuilder, la redoutable erreur some View vs any View) cessent d’être mystérieux.

Une vue SwiftUI est un type valeur qui se conforme à un seul protocole comportant une seule exigence. Le reste du framework est construit sur des fonctionnalités du langage Swift qui existent en dehors de SwiftUI : result builders, types opaques, génériques avec contraintes, property wrappers. Si vous comprenez ces fonctionnalités du langage, le framework se lit comme un TERM_18 Swift normal. Sinon, le framework se lit comme une magie qui mord parfois.

L’article parcourt le substrat. Il n’y a pas de LiveActivityManager ici, pas de capture d’écran de Get Bananas. L’objet, c’est le framework, pas un projet ; une fois le framework lisible, chaque article shipped-code de la série se lit plus clairement.

TL;DR

  • Une vue SwiftUI est un type valeur Swift conforme à View. Le protocole a une seule exigence : var body: some View { get }. Tout le reste est construit par-dessus des fonctionnalités du langage Swift.
  • @ViewBuilder est un result builder. Le body de toute View en est un. Les result builders transforment des expressions séparées par des virgules en une seule valeur de retour via des appels synthétisés par le compilateur.
  • some View est un type de retour opaque. Le compilateur connaît le type concret ; l’appelant non. Le type opaque est ce qui rend les bodies de vues rapides à la compilation et à l’exécution ; AnyView est l’échappatoire à effacement de type pour les cas où l’opacité ne fonctionne pas.
  • Group, EmptyView, TupleView, _ConditionalContent sont les types d’implémentation que les result builders synthétisent. Ils sont documentés mais rarement écrits à la main.

Le protocole qui démarre tout

Le protocole View a une seule exigence :1

public protocol View {
    associatedtype Body : View
    @ViewBuilder var body: Self.Body { get }
}

Deux parties de ce protocole comptent pour comprendre le reste de SwiftUI.

Le type associé Body : View. Le body d’une vue est lui-même une vue. La récursivité est ce qui rend le framework composable. Chaque View retourne une autre View depuis son body, et ainsi de suite, jusqu’à ce que la chaîne se termine sur l’une des vues primitives du framework (telles que Text, Color, Image, EmptyView) dont le Body est Never. Les vues primitives sont les feuilles de l’arbre ; les vues que vous écrivez sont les branches.

L’attribut @ViewBuilder sur body. Chaque body est une closure de result builder. Les result builders sont une fonctionnalité du langage Swift documentée dans SE-0289 (formalisée comme @resultBuilder dans Swift 5.4) qui permet à une closure contenant une séquence d’expressions d’être transformée par le compilateur en une seule valeur de retour via des appels de méthode synthétisés.2 Cette transformation est ce qui fait fonctionner la syntaxe sans virgules, façon instructions, à l’intérieur d’un body SwiftUI.

La forme du protocole est inhabituelle pour deux raisons.

D’abord, l’exigence est une propriété calculée, pas une méthode. Le body de la vue est recalculé à chaque passage de rendu lorsque SwiftUI décide que l’état de la vue a changé. Le framework considère que body est peu coûteux à appeler ; les calculs longs à l’intérieur de body sont un anti-pattern parce qu’ils s’exécutent à chaque rendu.

Ensuite, Self.Body est associé, pas effacé. Le type concret du body d’une vue fait partie de sa signature à la compilation. Le type body de Text("Hello") est Never ; le type body d’une vue personnalisée est ce que @ViewBuilder a synthétisé pour le body. Le design par type associé est ce qui permet au compilateur d’optimiser l’arbre de vues sans vérifications de type à l’exécution. C’est aussi ce qui crée l’exigence some View lorsqu’une vue personnalisée retourne du contenu conditionnel.

Result builders : le DSL sans virgules

Un result builder est une fonctionnalité du langage Swift qui transforme une closure en une seule valeur de retour en insérant des appels de méthode synthétisés par le compilateur. @ViewBuilder est un result builder. Le body de chaque vue SwiftUI est sa closure.2

Considérez cette vue :

struct ExampleView: View {
    var body: some View {
        Text("Title")
        Text("Subtitle")
        Image(systemName: "star")
    }
}

Le body comporte trois instructions sans séparateur. En Swift normal, c’est une erreur de compilation : une closure ne peut retourner qu’une seule valeur. Les result builders réécrivent la closure avant compilation. Le code réel que voit le compilateur, après expansion par @ViewBuilder, est en gros :

struct ExampleView: View {
    var body: some View {
        ViewBuilder.buildBlock(
            Text("Title"),
            Text("Subtitle"),
            Image(systemName: "star")
        )
    }
}

ViewBuilder.buildBlock(_:_:_:) est une méthode statique qui prend trois vues et retourne un TupleView<(Text, Text, Image)>. Le body retourne cette unique valeur tuple-view. Les anciennes versions de SwiftUI livraient un ensemble fixe de surcharges de buildBlock pour 1, 2, 3, … jusqu’à 10 enfants ; SwiftUI actuel utilise le support des génériques variadiques de Swift (buildBlock<each Content>) si bien qu’un body avec onze vues sœurs ou plus n’est plus un cas particulier.

Le même pattern gère le contrôle de flux. Un body de vue avec une instruction if ressemble à ceci :

struct ConditionalView: View {
    let isActive: Bool
    var body: some View {
        if isActive {
            Text("Active")
        } else {
            Text("Inactive")
        }
    }
}

@ViewBuilder le réécrit via des appels buildEither(first:) / buildEither(second:), produisant un _ConditionalContent<Text, Text>. Le compilateur connaît le type résultant à la compilation, même si une seule branche s’exécute lors d’un rendu donné.

if let, switch, le déballage d’optionnels et quelques autres constructions sont gérés par les diverses méthodes statiques buildXxx du result builder.3 Le contenu répété est le seul cas notable que la fonctionnalité du langage prend en charge via buildArray mais que @ViewBuilder ne prend pas en charge : une boucle for brute à l’intérieur d’un body échoue avec « closure containing control flow statement cannot be used with result builder ‘ViewBuilder’. » La réponse façon SwiftUI est ForEach, qui prend un RandomAccessCollection et une closure de contenu, et synthétise l’itération en une unique vue à type valeur. Le DSL n’est pas sur mesure ; ce sont les result builders Swift, configurés pour les vues.

some View et le problème de l’opacité

Le body d’une vue personnalisée retourne généralement some View. Le mot-clé désigne le type de retour opaque et a été ajouté dans Swift 5.1.4

some View dit : « Je retourne un type spécifique conforme à View, mais je ne vous dis pas lequel. » Le compilateur suit le type concret en interne pour l’optimisation ; l’appelant de votre vue ne voit que le témoin de protocole. Ce pattern est ce qui permet au body d’une vue de retourner un type complexe comme VStack<TupleView<(Text, Image, Spacer)>> sans vous obliger à écrire ce type dans votre code source.

Deux choses à propos de some View qui déroutent les nouveaux développeurs SwiftUI :

some View est un type spécifique unique, même quand vous retournez des choses différentes. L’expression if condition { Text("A") } else { Image("b") } est autorisée à l’intérieur d’un body @ViewBuilder parce que le result builder enveloppe les deux branches dans _ConditionalContent, produisant un seul type concret. Mais l’expression if condition { return Text("A") } else { return Image("b") } en dehors d’un result builder est une erreur de compilation : les deux branches retournent des types concrets différents, et some View en exige un seul. Les result builders sont ce qui fait fonctionner les formes de retour conditionnelles ; les retours explicites perdent la transformation par result builder.

some View n’est pas la même chose que any View. some View est opaque (un type spécifique unique, masqué) ; any View est existentiel (une boîte qui peut contenir n’importe quel type conforme, avec un coût à l’exécution). Une fonction libre ou une propriété peut légalement retourner any View. Le body du protocole View, en revanche, ne le peut pas : le protocole exige associatedtype Body: View, et any View ne se conforme pas lui-même à View, donc var body: any View ne satisfait pas le protocole et le compilateur suggère some View. La règle pratique : utilisez some View pour les bodies de vues, recourez à AnyView (le wrapper à effacement de type) pour les types de vues qui varient à l’exécution. Le message d’erreur « function declares an opaque return type but the return statements in its body do not have matching underlying types » signifie presque toujours que vous avez essayé de retourner différents types concrets depuis une fonction some View et que vous avez besoin soit de la branchage par result builder, soit de AnyView.

AnyView : l’échappatoire

AnyView est un wrapper de vue à effacement de type. La construction est AnyView(myView). Le wrapper contient n’importe quelle vue conforme, et SwiftUI l’accepte là où une View est attendue.5

Le cas d’usage de l’échappatoire est une fonction qui retourne différents types concrets selon des données à l’exécution et qui ne peut pas être exprimée via le branchage par result builder :

func viewForKind(_ kind: Kind) -> AnyView {
    switch kind {
    case .text: return AnyView(Text("hello"))
    case .image: return AnyView(Image("photo"))
    case .custom: return AnyView(MyCustomView())
    }
}

Le coût de AnyView est que le type sous-jacent ne fait pas partie de l’identité statique de la vue. La documentation d’Apple décrit la conséquence directement : lorsque le type enveloppé à l’intérieur d’un AnyView change entre les rendus, la hiérarchie de vues existante est détruite et une nouvelle hiérarchie est créée à sa place, ce qui signifie perte d’état, animations redémarrées et perte d’identité. Réenvelopper le même type concret ne déclenche pas cette destruction, mais le diffing piloté par les types statiques que le framework préfère n’est plus disponible non plus.

La bonne règle est : préférez le branchage par result builder @ViewBuilder pour les vues conditionnelles (if, switch, for), préférez les vues paramétrées pour les types variables, ne recourez à AnyView que lorsque ni l’un ni l’autre ne fonctionnent. Une func viewForKind qui retourne AnyView est généralement le signe que vous devriez faire en sorte que viewForKind retourne some View et placer un switch à l’intérieur d’une closure de result builder.

Group, EmptyView, TupleView : les types d’implémentation

Le result builder synthétise des types de vue concrets spécifiques. Trois d’entre eux méritent d’être reconnus :6

Group est un conteneur transparent. Il accepte jusqu’à dix vues comme contenu et les présente comme des sœurs au layout parent. Le conteneur lui-même n’ajoute aucune structure visuelle ; les contenus s’affichent exactement comme s’ils l’étaient individuellement. Le cas d’usage est l’enveloppement de plusieurs vues dans un contexte qui attend une seule vue (un modificateur .if, un retour conditionnel, une fonction qui produit « une seule vue »). Group { Text("A"); Text("B") } est une seule vue contenant deux ; c’est la forme explicite de ce que les result builders font implicitement.

EmptyView est une vue qui n’affiche rien. Le result builder l’utilise comme branche conditionnelle fausse lorsqu’un if n’a pas d’else. Retourner EmptyView() depuis votre propre code est un moyen de renoncer au rendu sans changer le type de retour de la fonction.

TupleView est le type concret que les result builders produisent lorsqu’un body comporte plusieurs vues sœurs. L’expression en haut de cet article qui retourne trois vues sœurs retourne en réalité un TupleView<(Text, Text, Image)>. Vous n’écrivez presque jamais TupleView directement ; vous le lisez dans les messages d’erreur.

_ConditionalContent (avec le tiret bas en préfixe) est le type qui gère les branches if/else. Le type apparaît dans la surface publique de ViewBuilder, mais le nom souligné signale « ne pas écrire contre ceci à la légère » ; laissez le result builder le synthétiser depuis if/else plutôt que de le construire à la main.

@ViewBuilder sur vos propres fonctions

Les result builders ne sont pas réservés à body. N’importe quelle fonction ou paramètre de closure peut être annoté @ViewBuilder, et la même syntaxe DSL devient légale à l’intérieur.2

struct Card<Content: View>: View {
    let content: Content

    init(@ViewBuilder content: () -> Content) {
        self.content = content()
    }

    var body: some View {
        VStack(alignment: .leading, spacing: 8) {
            content
        }
        .padding()
        .background(.regularMaterial)
        .clipShape(RoundedRectangle(cornerRadius: 12))
    }
}

// Usage: callers get the result-builder DSL inside the closure.
Card {
    Text("Title")
    Text("Subtitle")
    Image(systemName: "star")
}

Ce pattern est la manière dont VStack, HStack, ZStack, List, Form, Section, Group et NavigationStack de SwiftUI acceptent plusieurs enfants. Chacun de ces types prend un paramètre @ViewBuilder content: () -> Content. Reconnaître ce pattern signifie que vous pouvez écrire vos propres vues conteneurs avec la même ergonomie que celles du framework, sans aucun support spécial du compilateur requis.

La raison pour laquelle vous écrivez init(@ViewBuilder content:) et pas simplement init(content:) est que l’attribut sur le paramètre est ce qui active la transformation par result builder à l’intérieur du body de closure que l’appelant fournit. Sans l’attribut, Card { Text("A"); Text("B") } est une erreur de compilation parce que la closure a deux instructions et pas de @ViewBuilder pour les transformer.

State, Bindings et la couche des property wrappers

Tout ce qui précède concerne la forme de l’arbre de vues. L’autre moitié de SwiftUI, c’est le state, et cette moitié est construite sur les property wrappers Swift.7

Les property wrappers les plus pertinents pour l’écriture de vues :

@State possède un morceau d’état à type valeur à l’intérieur d’une seule vue. Lire la propriété lit le stockage sous-jacent ; lui assigner une valeur déclenche un nouveau rendu de la vue. Le wrapper est approprié pour un état simple, local à la vue (l’état on/off d’un toggle, la chaîne brouillon d’un champ de texte).

@Binding est une référence bidirectionnelle vers l’état d’une autre vue. Une vue enfant qui doit lire et écrire l’état d’un parent prend un paramètre Binding<T>. Le parent construit le binding via $state (la projection avec signe dollar sur @State).

@Observable (iOS 17+) est une macro qui remplace l’ancien pattern de conformité à ObservableObject. La macro appliquée à une classe génère le suivi du framework Observation de sorte que les propriétés de la classe déclenchent des nouveaux rendus de vues lorsqu’elles sont lues à l’intérieur d’un body puis modifiées plus tard. La possession côté vue d’une classe @Observable passe de @StateObject à un simple @State ; les vues en aval qui ont besoin d’une poignée bidirectionnelle utilisent @Bindable au lieu de @ObservedObject.

@Environment lit les valeurs injectées par dépendance depuis la chaîne d’environnement. SwiftUI fournit des clés d’environnement intégrées (locale, color scheme, action de fermeture) ; les applications ajoutent des clés personnalisées pour l’injection de dépendances spécifique au domaine.

La couche des property wrappers est ce qui permet au body d’une vue de se réexécuter lorsque l’état change. SwiftUI suit les lectures à l’intérieur de body à travers deux mécanismes distincts : AttributeGraph (le graphe de dépendances privé d’Apple qui sous-tend @State, @Binding et @Environment) pour l’ancien chemin par property wrapper, et le framework Observation de la bibliothèque standard (withObservationTracking, public à partir d’iOS 17+) pour les types @Observable.8 Lorsqu’une propriété suivie est mutée, les bodies correspondants sont réexécutés et la machinerie de diffing calcule le changement minimal de l’arbre de vues.

Les deux moitiés (la couche d’arbre de vues et la couche d’état) sont faiblement couplées. L’arbre de vues est à types valeur et rapide à recalculer. La couche d’état est à type référence (pour @Observable) ou à type valeur avec pointeur de stockage (pour @State) et suit les lectures. Ensemble, elles produisent le modèle du framework : « décrivez ce qui devrait être à l’écran en fonction de l’état, et le framework calcule le diff ».

Ce que vous reconnaissez désormais dans les messages d’erreur

Lire les erreurs du compilateur SwiftUI avec le substrat visible :

« Function declares an opaque return type, but the return statements in its body do not have matching underlying types. » Deux instructions return avec des types concrets différents dans une fonction some View. Correctif : utilisez @ViewBuilder pour que le result builder enveloppe les deux dans _ConditionalContent, ou enveloppez les deux retours dans AnyView.

« The compiler is unable to type-check this expression in reasonable time. » De longues chaînes de body avec de nombreux modificateurs épuisent le vérificateur de types. Correctif : décomposez le body en propriétés calculées plus petites ou en sous-vues ; chaque morceau retournant some View simplifie le travail d’inférence.

« Cannot convert value of type ‘TupleView<…>’ to expected type ‘some View’. » Une fonction attendant une seule vue a reçu le résultat d’un body multi-instructions sans @ViewBuilder. Correctif : ajoutez @ViewBuilder au paramètre de closure acceptant le contenu multi-instructions.

« Generic parameter ‘Content’ could not be inferred. » Un conteneur personnalisé prend @ViewBuilder content: () -> Content et le site d’appel a une closure vide. Correctif : les result builders ont besoin d’au moins une expression pour inférer Content ; les closures vides retombent sur EmptyView() si le site d’appel le fournit explicitement.

Les messages d’erreur sont peu amicaux parce que le substrat est invisible. Les lire avec le substrat visible transforme la plupart d’entre eux en « ah, le result builder ne peut pas transformer ceci » ou « ah, j’ai besoin soit de branchage soit de AnyView ».

Quand sortir du substrat

Quelques patterns que le substrat ne gère pas proprement :

Types concrets variadiques. Une fonction qui retourne un type View différent par branche et que vous ne pouvez pas envelopper dans un branchage par result builder a besoin de AnyView. Acceptez le coût (perte de diffing, pas d’animation) et documentez le site d’appel.

Vues conditionnelles cross-platform. Le #if os(iOS) à la compilation fonctionne à l’intérieur d’un body @ViewBuilder mais limite le nombre de branches du result builder ; les bodies conditionnels multi-OS atteignent parfois la limite « expression too complex ». Le correctif consiste à extraire les sous-vues spécifiques à une plateforme dans des fonctions séparées, retournant chacune some View.

Construction impérative de vues. Le framework attend que les vues soient des expressions, pas des objets construits puis mutés. Le style UIKit « créer le label, définir le texte, ajouter à la subview » ne se traduit pas ; l’équivalent SwiftUI est un Text("...") à type valeur retourné depuis un body. Les patterns qui exigent une construction impérative sont généralement le signe que le travail relève d’un pont UIViewRepresentable vers UIKit.

Ce que ce pattern signifie pour les apps qui livrent sur iOS 26+

Trois enseignements.

  1. SwiftUI est du Swift, pas de la magie. Les result builders, les types de retour opaques et les property wrappers sont tous dans la référence du langage Swift. Lire le framework comme du code Swift, et non comme un DSL spécial, rend les parties surprenantes prévisibles.

  2. some View et AnyView résolvent des problèmes différents. Les types de retour opaques sont la valeur par défaut ; l’effacement de type est l’échappatoire. Recourir à AnyView devrait être le cas rare ; recourir à some View plus le branchage par result builder devrait être le cas courant.

  3. Les result builders sont l’intégralité du DSL. Partout où une fonction ou un paramètre est @ViewBuilder, la syntaxe sans virgules, façon instructions, est disponible. Écrire vos propres vues conteneurs avec la même ergonomie que VStack représente un attribut et un paramètre de closure.

Associez cet article à la série shipped-code de la collection : SwiftUI cross-platform (Return tourne sur cinq plateformes avec un seul cœur SwiftUI partagé) ; la couche visuelle Liquid Glass ; la machine à états des Live Activities sur iOS ; le contrat du runtime watchOS sur Apple Watch. Le hub se trouve à la Série Apple Ecosystem. Pour un contexte plus large iOS-avec-agents-IA, consultez le guide iOS Agent Development.

FAQ

Qu’est-ce que le protocole View dans SwiftUI ?

Le protocole View a une seule exigence : var body: some View { get }. Chaque vue SwiftUI est un type valeur Swift conforme à View, avec une propriété calculée body qui retourne une autre vue (ou Never pour les vues primitives comme Text, Color, Image, EmptyView). Le body est annoté @ViewBuilder afin qu’il puisse utiliser la syntaxe DSL sans virgules de SwiftUI.

Que signifie some View ?

some View est un type de retour opaque (Swift 5.1+). Le compilateur connaît le type concret ; l’appelant ne voit que le témoin de protocole. Les types opaques permettent aux bodies de vues de retourner des types complexes comme VStack<TupleView<(Text, Image, Spacer)>> sans les épeler, tout en préservant l’optimisation à la compilation. some View est un type spécifique unique, même si ce type n’est pas visible au site d’appel.

Quand devrais-je utiliser AnyView ?

N’utilisez AnyView que lorsque ni le branchage par result builder @ViewBuilder (if, switch, for) ni les génériques paramétrés ne résolvent le problème. Lorsque le type concret enveloppé change entre les rendus, la hiérarchie de vues existante est détruite et une nouvelle est créée à sa place ; c’est le moment où les animations redémarrent et où l’état des vues est réinitialisé. Réenvelopper le même type concret ne déclenche pas cette destruction, mais le diffing piloté par les types statiques que le framework préfère n’est plus disponible non plus. Si vous vous retrouvez à recourir souvent à AnyView, le pattern qui doit changer est en amont : préférez les vues paramétrées ou poussez la condition dans un body de result builder.

Qu’est-ce que @ViewBuilder et où puis-je l’utiliser ?

@ViewBuilder est un result builder (fonctionnalité du langage Swift). Il transforme une closure comportant plusieurs expressions en une seule valeur de retour en insérant des appels buildBlock, buildEither, buildOptional, etc. synthétisés par le compilateur. Le body de chaque vue SwiftUI est @ViewBuilder par défaut. Vous pouvez appliquer @ViewBuilder à n’importe quelle fonction ou paramètre de closure pour donner aux appelants la même syntaxe DSL ; VStack, Card et Section utilisent le même pattern pour accepter plusieurs enfants.

Pourquoi le body de ma vue se réaffiche-t-il alors que je ne m’y attendais pas ?

SwiftUI réexécute body chaque fois qu’une propriété d’état que le body lit est mutée. Les property wrappers (@State, @Binding, @Observable, @Environment) suivent les lectures et déclenchent des nouveaux rendus lors des écritures. Les rendus inattendus se ramènent généralement à un changement d’état dans une vue parente, à un changement de valeur d’environnement ou à la modification d’une propriété lue d’un objet @Observable. Le diffing du framework calcule alors le changement minimal de l’arbre.

Références


  1. Apple Developer, « View » et « Configuring views ». Le protocole View, le type associé Body et l’attribut @ViewBuilder sur body

  2. Swift Evolution, « SE-0289: Result builders ». La proposition de langage qui a formalisé les result builders (introduits comme _functionBuilder en 5.1, formalisés comme @resultBuilder en 5.4). Définit buildBlock, buildEither, buildOptional, buildArray, buildExpression, buildFinalResult et compagnie. 

  3. Apple Developer, « ViewBuilder » et « ForEach ». Le type de result builder que SwiftUI utilise pour les bodies de vues (buildBlock à génériques variadiques, buildEither, déballage d’optionnels). ViewBuilder n’expose pas buildArray, donc ForEach est la primitive d’itération pour répéter une vue sur une collection. 

  4. Swift Evolution, « SE-0244: Opaque result types ». Le mot-clé some pour les types de retour opaques, ajouté dans Swift 5.1. 

  5. Apple Developer, « AnyView ». Wrapper de vue à effacement de type, construction et le compromis de diffing. 

  6. Apple Developer, « Group », « EmptyView » et « TupleView ». Types d’implémentation que les result builders synthétisent. 

  7. Apple Developer, « State and Data Flow ». La couche des property wrappers : @State, @Binding, @Observable, @Environment. Le système d’observation de SwiftUI et la macro @Observable à partir d’iOS 17+. 

  8. Apple Developer, « Observation » et « Migrating from the Observable Object protocol to the Observable macro ». Le framework Observation de la bibliothèque standard, y compris withObservationTracking(_:onChange:), plus le chemin de migration iOS 17 de ObservableObject vers @Observable

Articles connexes

watchOS Runtime Is a Contract, Not a Background Task

watchOS does not have iOS's background. WKExtendedRuntimeSession is a contract you sign with the system, broken on wrist…

15 min de lecture

RealityKit And The Spatial Mental Model

RealityKit is an entity-component-system, not SwiftUI in 3D. Anchors place entities in real space. Five ways the model d…

16 min de lecture

The Cleanup Layer Is the Real AI Agent Market

Charlie Labs pivoted from building agents to cleaning up after them. The AI agent market is moving from generation to pr…

15 min de lecture