Déclarez le contenu fourni par l’appelant sous la forme (Input) -> Content, avec Content contraint à View. Cela permet à un composant macOS de fournir une valeur typée tandis que les appelants choisissent sa présentation. Le contrat exige que cette entrée soit acceptée ; il ne peut pas imposer qu’elle soit utilisée.

Ce tutoriel construit un petit aperçu de nuance pour ordinateur de bureau. Le composant réutilisable possède un contrôle de taille et fournit une configuration synthétique à la fermeture de rendu de l’appelant. L’appelant dessine la nuance sans gérer l’état interne du composant.

Quelle interface exprime cette exigence ?

Utilisez un paramètre générique nommé pour la vue renvoyée et un type concret pour l’entrée du composant. Dans l’exemple ci-dessous, la signature essentielle est @ViewBuilder content: @escaping (SwatchInput) -> Content, au sein d’une déclaration contrainte par Content: View.

L’entrée et la sortie ont des rôles distincts. SwatchInput définit les valeurs fournies par le composant. Content représente le type de vue concret produit par la fermeture de chaque appelant. Les paramètres génériques nommés permettent à une déclaration d’exprimer et de réutiliser ces relations entre types, comme l’explique le chapitre Generics de Swift.

Apple utilise ce modèle d’interface dans GeometryReader : son initialiseur accepte une fermeture de type (GeometryProxy) -> Content. Le conteneur fournit le proxy et l’appelant crée du contenu à partir de celui-ci. C’est un précédent utile pour un composant personnalisé qui fournit sa propre configuration. Consultez la documentation d’Apple sur GeometryReader et init(content:).

Choisissez le contrat en fonction de la tâche réelle :

Exigence Interface adaptée
L’appelant fournit du contenu sans entrée du composant () -> Content
Le composant fournit une configuration précise (SwatchInput) -> Content
Le conteneur reçoit une valeur dont le type varie selon les utilisations Un paramètre générique nommé Input, avec une fermeture (Input) -> Content correspondante
Le compilateur doit prouver que l’appelant utilise la configuration Une signature de fermeture n’exprime pas cette exigence

Ces choix découlent des types de fonction de Swift : leurs types de paramètres et leur type de retour décrivent ce qui peut être transmis et renvoyé. Ils ne décrivent pas la façon dont l’implémentation doit utiliser ses arguments. Consultez la section Function Type de la référence du langage Swift.

Créer un aperçu de nuance macOS complet

Utilisez une cible d’app macOS avec SwiftUI. L’exemple emploie la syntaxe établie @ViewBuilder et vise Swift 5.7 ou une version ultérieure, avec une cible de déploiement macOS 12 ou ultérieure. Il évite les macros d’aperçu et les noms plus récents de constructeurs de contenu. Swift et le SDK Apple ne sont pas disponibles dans l’environnement de recherche ; ce programme a donc été inspecté, mais n’a pas été compilé ni exécuté ici.

Dans un projet d’app SwiftUI Xcode, remplacez la déclaration d’app générée par le programme complet suivant. Supprimez la déclaration ContentView générée si elle ferait double emploi. Gardez un seul point d’entrée d’app marqué @main. L’aide Xcode d’Apple explique comment démarrer avec un projet SwiftUI ou ajouter un fichier SwiftUI à un projet existant.

import SwiftUI

struct SwatchInput {
    let title: String
    let side: CGFloat
    let color: Color
}

@MainActor
struct SwatchPanel<Content: View>: View {
    @State private var isLarge = false
    private let content: (SwatchInput) -> Content

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

    private var input: SwatchInput {
        SwatchInput(
            title: "Sample A",
            side: isLarge ? 120 : 64,
            color: .blue
        )
    }

    var body: some View {
        VStack(alignment: .leading, spacing: 16) {
            Toggle("Large sample", isOn: $isLarge)
            content(input)
        }
        .padding(24)
    }
}

@MainActor
struct ContentView: View {
    var body: some View {
        SwatchPanel { input in
            Text(input.title)
                .font(.headline)

            RoundedRectangle(cornerRadius: 12)
                .fill(input.color)
                .frame(width: input.side, height: input.side)

            if input.side == 120 {
                Text("Large preview")
                    .font(.caption)
            }
        }
        .frame(width: 300, height: 280, alignment: .topLeading)
    }
}

@main
@MainActor
struct TypedContentDemoApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

L’affichage initial attendu contient le bouton Large sample, le titre Sample A et un carré bleu arrondi de 64 points de côté. Activer le bouton fait passer la longueur de côté fournie à 120 points et ajoute Large preview. Ces tailles et libellés sont des valeurs locales illustratives, et non des résultats mesurés.

Le composant possède isLarge. Sa propriété calculée input regroupe la sélection actuelle, et content(input) transmet ce regroupement à l’appelant. SwiftUI gère le stockage de l’état et met à jour les parties de la hiérarchie de vues qui dépendent d’une valeur modifiée, comme l’explique la documentation State.

L’appelant gère la présentation : un titre, une forme et une légende facultative. Il peut adopter une autre disposition tout en conservant la même entrée typée. Le panneau et son appelant implémentent tous deux body pour composer leurs interfaces, conformément au modèle View documenté par Apple.

Pourquoi les annotations de constructeur et d’échappement sont-elles nécessaires ?

@ViewBuilder permet à l’appelant d’écrire plusieurs expressions de vues et des branches conditionnelles prises en charge dans une seule fermeture. Dans cet exemple, l’appelant fournit deux vues inconditionnelles et une légende conditionnelle. Le constructeur combine ce contenu en un résultat qui respecte la contrainte de retour du composant. Apple documente les vues enfants multiples et la construction conditionnelle dans ViewBuilder.

Le constructeur ne fournit pas SwatchInput. Le paramètre ordinaire de la fermeture déclare l’entrée, et content(input) fournit l’argument réel. Si l’initialiseur acceptait () -> Content, l’ajout d’une annotation de constructeur ne le transformerait pas en fermeture acceptant une configuration.

@escaping est nécessaire parce que l’initialiseur stocke la fermeture dans une propriété afin de l’appeler plus tard depuis body. Le chapitre Closures de Swift explique qu’une fermeture s’échappe lorsqu’elle peut être appelée après le retour de la fonction qui la reçoit.

Pour un composant dont la configuration est déjà définitive lors de l’initialisation, une autre conception consiste à appeler immédiatement un constructeur sans échappement et à stocker la valeur Content obtenue. Cela change le moment où la configuration est appliquée. Ici, conserver la fermeture rend le flux prévu explicite : dériver l’entrée actuelle de l’état du composant, puis construire le contenu avec cette entrée.

Respecter la frontière des constructeurs Xcode

La documentation actuelle d’Apple distingue Xcode 26 et les versions antérieures de Xcode 27 et ultérieures. Pour les outils antérieurs, elle décrit ViewBuilder comme construisant des vues. À partir de Xcode 27, elle décrit une construction indépendante du type et recommande l’alias ContentBuilder pour indiquer ce comportement plus général. Consultez ViewBuilder.

Lors de la maintenance d’une base de code Xcode 27 ou ultérieure, l’initialiseur peut aussi utiliser @ContentBuilder. Apple le documente comme un alias de type pour ViewBuilder, la conformité étant vérifiée au moyen des types de contenu qu’il produit. Conservez Content: View dans ce composant : sa sortie est toujours destinée à une hiérarchie de vues. Ne supposez pas que cette syntaxe plus récente existe dans les SDK plus anciens simplement parce que la documentation actuelle indique des cibles de déploiement antérieures. Consultez ContentBuilder.

Pourquoi some View ne résout-il pas tous les aspects de ce contrat ?

some View convient aux propriétés body de l’exemple. Il masque le type de résultat concret tout en garantissant que le résultat est une vue. La fermeture stockée utilise plutôt le paramètre nommé Content afin que le composant puisse faire référence au même type de résultat dans sa propriété et son initialiseur.

Swift 5.7 a introduit les déclarations de paramètres opaques via SE-0341. Un paramètre direct tel que _ value: some View introduit un paramètre générique sans nom dont le type concret est déterminé par l’appelant. Cela ne signifie pas que l’implémentation de la fonction peut choisir une vue quelconque et la transmettre partout où ce type sans nom est attendu. Consultez Opaque Parameter Declarations.

En particulier, un paramètre de rappel écrit sous la forme (some View) -> Content ne fait pas partie des positions prises en charge pour les paramètres opaques et documentées par SE-0341. La proposition interdit explicitement les types opaques dans la position d’entrée du type de fonction d’un rappel. La syntaxe évoquée pour une éventuelle prise en charge figure dans les évolutions futures, et non dans les fonctionnalités implémentées. La référence actuelle Types maintient cette restriction.

Pour cette tâche sur ordinateur de bureau, la solution prise en charge est simple : faites de la valeur fournie par le composant un type de configuration concret. Si un autre conteneur réutilisable doit préserver le type d’entrée fourni par l’appelant, nommez ce type au moyen d’un paramètre générique et transmettez-le de manière cohérente dans la valeur stockée et le rappel. Aucune de ces conceptions ne nécessite une entrée de rappel opaque.

Quelles garanties le compilateur offre-t-il réellement ?

La signature garantit une forme appelable : un argument SwatchInput et un résultat respectant Content: View. Elle donne aussi son type inféré au paramètre de la fermeture, de sorte que l’appelant peut accéder à input.title, input.side et input.color sans répéter d’annotation de type. Swift documente cette inférence à partir du contexte de la fermeture dans Closures.

Elle ne peut pas garantir que ces propriétés influencent l’affichage. Par exemple, l’expression suivante peut remplacer le contenu de ContentView.body ; elle accepte et ignore délibérément la configuration :

SwatchPanel { _ in
    Text("Fixed preview")
}

Cette limite est importante lors de la rédaction des exigences du composant. « Le contenu de l’appelant accepte SwatchInput » est un contrat de type. « Le contenu de l’appelant affiche la taille fournie » est une exigence comportementale. Si l’affichage de cette taille est essentiel au composant, intégrez ce rendu dans le composant et exposez une zone de personnalisation plus restreinte.

Utilisez les vérifications suivantes pour diagnostiquer les problèmes d’intégration. Il s’agit d’étapes d’inspection déduites de l’interface, et non de résultats de tests du compilateur rapportés :

Symptôme Vérification
Argument manquant lors de l’appel de content Passez la configuration : content(input).
Diagnostic de fermeture avec échappement dans l’initialiseur Conservez @escaping lorsque vous stockez le rappel.
Échec de plusieurs vues sœurs dans le contenu de l’appelant Conservez l’annotation de constructeur appropriée sur le paramètre de l’initialiseur.
Un rappel explicitement typé est rejeté Vérifiez que son entrée est SwatchInput, et non un type sans rapport comme String.
L’aperçu ne tient pas compte du contrôle de taille Vérifiez si le rendu lit input.side.
Le composant continue d’afficher la configuration initiale Vérifiez si le contenu a été construit une seule fois et stocké, au lieu d’appeler le rappel avec l’entrée actuelle.

Limitez le contrat à ce que la tâche exige. Une configuration concrète et un résultat de vue générique donnent à ce panneau de nuances une frontière claire : le composant fournit les valeurs actuelles, l’appelant les reçoit avec toutes les informations de type et la présentation choisie reste facile à remplacer.

Sources