Pour recevoir différentes complétions typées dans un seul écran macOS SwiftUI, donnez à chaque composant un adaptateur générique ou une fermeture typée. Swift ne permet pas à l'écran de se conformer au même protocole deux fois avec des types associés différents.

Ce tutoriel construit un petit aperçu de bureau dont les boutons appliquent un titre synthétique et un nombre de colonnes. La tâche consiste à maintenir le contrat de complétion de chaque composant vérifié par le compilateur tout en permettant à une vue parente de recevoir les deux valeurs.

Pourquoi un type ne peut-il pas adopter les deux contrats de complétion ?

Le type associé d'un protocole appartient à sa conformance. Si un protocole de complétion déclare un type associé nommé Value, un type conforme doit fournir un choix cohérent pour ce type. L'ajout de deux méthodes de complétion surchargées ne crée pas deux conformances indépendantes. Le guide du langage Swift explique comment les types associés acquièrent leurs types concrets lors de l'adoption d'un protocole. Generics — Associated Types.

Par exemple, un écran ne peut pas adopter un protocole de complétion une fois avec Value égal à TitleChoice et à nouveau avec Value égal à LayoutChoice. Déplacer les déclarations dans des extensions séparées ne change pas cette restriction. SE-0143 préserve explicitement l'interdiction de conformance répétée, y compris les conformances conditionnelles dont les conditions sont disjointes. Conditional conformances.

Les types associés principaux ne suppriment pas cette frontière. La syntaxe CompletionHandling<TitleChoice> exprime une contrainte sur un type associé ; elle ne crée pas d'identité de protocole distincte pour chaque argument. SE-0346 a introduit cette syntaxe de contrainte dans Swift 5.7. Lightweight same-type requirements for primary associated types.

La conception prise en charge ci-dessous déplace la conformance sur un adaptateur générique. Il y a une seule déclaration de conformance, et l'application crée des valeurs d'adaptateur différemment spécialisées pour les deux composants. Le parent possède l'état résultant sans adopter le protocole de complétion.

Quel design de complétion convient à votre composant ?

Utilisez l'aide à la décision suivante avant de modifier l'interface. Ce sont des recommandations de conception dérivées des règles de types documentées, et non des exigences supplémentaires du langage Swift.

Exigence du composant Design suggéré Ce qui reste explicite
Un rappel, avec une interface que vous contrôlez Une fermeture typée Le type d'entrée du rappel
Une API existante basée sur un protocole Un adaptateur conforme générique Le protocole et son type associé
Un canal d'événement transportant plusieurs alternatives nommées Un enum avec valeurs associées Chaque cas d'événement géré par le récepteur
Plusieurs opérations sans rapport avec des significations différentes Protocoles nommés distincts Les exigences distinctes de chaque opération

Pour cet exemple, supposons que le composant réutilisable attend déjà un gestionnaire basé sur un protocole. Un adaptateur générique préserve cette frontière et garde le code de gestion de l'écran à côté de ses mises à jour d'état. Si le composant n'a besoin que d'un rappel et n'a aucune exigence de protocole, une fermeture est le design le plus petit.

Une méthode de complétion générique exprimerait une promesse différente. Une méthode telle que complete<T>(_ value: T) doit fonctionner pour chaque type permis par ses contraintes. Elle n'exprime pas l'exigence plus étroite selon laquelle chaque composant fournit un type de résultat particulier. La documentation sur les génériques de Swift décrit comment les fonctions génériques opèrent sur les types sélectionnés à leurs sites d'appel. Generics — Generic Functions.

Construire l'adaptateur de complétion typé

Utilisez une cible d'application macOS SwiftUI avec un compilateur Swift 5.7 ou ultérieur et une cible de déploiement macOS 12 ou ultérieure pour cet exemple. L'exigence du compilateur vient de la déclaration de type associé principal. La cible de déploiement est une base conservatrice pour ce tutoriel, pas une affirmation que chaque API montrée est apparue pour la première fois dans macOS 12.

Les trois fichiers ci-dessous forment ensemble l'exemple complet. Ajoutez-les à la même cible d'application, en remplaçant le point d'entrée d'application généré et la vue de contenu afin que la cible n'ait qu'une seule déclaration @main. Aucun paquet, asset, fichier d'exemple ou entrée externe n'est nécessaire.

Créez CompletionContracts.swift avec ces déclarations :

import SwiftUI

struct TitleChoice {
    let text: String
}

struct LayoutChoice {
    let columns: Int
}

@MainActor
protocol CompletionHandling<Value> {
    associatedtype Value
    func complete(_ value: Value)
}

@MainActor
struct CompletionAdapter<Output>: CompletionHandling {
    typealias Value = Output

    private let receive: @MainActor (Output) -> Void

    init(_ receive: @escaping @MainActor (Output) -> Void) {
        self.receive = receive
    }

    func complete(_ value: Output) {
        receive(value)
    }
}

@MainActor
struct CompletionButton<Value, Handler: CompletionHandling>: View
where Handler.Value == Value {
    let title: String
    let value: Value
    let handler: Handler

    var body: some View {
        Button(action: {
            handler.complete(value)
        }) {
            Text(title)
        }
    }
}

La contrainte Handler.Value == Value relie la sortie du composant à l'entrée acceptée par le gestionnaire. Un bouton transportant un TitleChoice nécessite donc un gestionnaire dont le type associé est TitleChoice. L'alias de type explicite rend le type associé de l'adaptateur facile à inspecter.

L'adaptateur stocke sa fermeture pour une invocation ultérieure, donc son initialiseur marque ce paramètre @escaping. La documentation sur les fermetures de Swift exige cette annotation lorsqu'une fermeture peut survivre à l'appel de fonction qui la reçoit. Closures — Escaping Closures.

Le protocole, l'adaptateur et la vue utilisent @MainActor, et le rappel stocké préserve cet isolement dans son type de fonction. Cela fait du contexte d'exécution UI prévu une partie du contrat. SE-0316 documente à la fois l'isolation d'acteur global et les types de fonctions qualifiés par acteur global. Global actors.

Apple documente Button comme prenant une action et un libellé. Ici, l'action invoque le gestionnaire avec la valeur déjà typée du composant ; Text(title) fournit son libellé visible. Button.

Connecter deux types de résultats à un seul écran SwiftUI

Créez ContentView.swift. Les résultats de titre et de mise en page ont des destinations d'état séparées, et chaque fermeture d'adaptateur met à jour seulement sa destination correspondante.

import SwiftUI

@MainActor
struct ContentView: View {
    @State private var previewTitle = "Sample Board"
    @State private var columnCount = 1

    var body: some View {
        VStack(alignment: .leading, spacing: 16) {
            Text("Component Preview")
                .font(.headline)

            Text(previewTitle)
            Text("Columns: \(columnCount)")

            HStack {
                CompletionButton(
                    title: "Apply title",
                    value: TitleChoice(text: "Pebble Board"),
                    handler: CompletionAdapter<TitleChoice> { choice in
                        previewTitle = choice.text
                    }
                )

                CompletionButton(
                    title: "Apply layout",
                    value: LayoutChoice(columns: 3),
                    handler: CompletionAdapter<LayoutChoice> { choice in
                        columnCount = choice.columns
                    }
                )
            }
        }
        .padding(24)
        .frame(minWidth: 420, minHeight: 180)
    }
}

Créez TypedCompletionsApp.swift comme unique point d'entrée de l'application :

import SwiftUI

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

Le parent conserve ses valeurs initiales dans des propriétés privées @State. Apple documente que SwiftUI gère ce stockage et met à jour les vues dépendantes quand les valeurs changent. Sa documentation actuelle note également que les builds avec Xcode 27 ou ultérieur utilisent la macro State à la place ; le style de déclaration montré ici suit la base line du property wrapper antérieur. State.

WindowGroup héberge la hiérarchie de vues de l'écran. Apple documente un stockage d'état indépendant pour chaque fenêtre créée par le groupe, donc l'état de cet exemple appartient à chaque fenêtre d'aperçu. WindowGroup.

Il y a deux types d'adaptateur spécialisés : CompletionAdapter<TitleChoice> et CompletionAdapter<LayoutChoice>. Les deux proviennent de la seule déclaration d'adaptateur générique. ContentView ne se conforme qu'à View ; il reçoit les deux résultats via les fermetures qu'il fournit.

Vérifier le comportement et diagnostiquer les incompatibilités

Aucun compilateur Swift n'est installé dans l'environnement de recherche, donc ces fichiers ont été inspectés mais n'ont pas été compilés ni exécutés. Construisez la cible d'application sur votre Mac, puis utilisez les vérifications suivantes pour vérifier le comportement prévu de l'exemple.

  1. Lancez l'application. L'aperçu devrait afficher Sample Board et Columns: 1.
  2. Cliquez sur Apply title. Le titre devrait devenir Pebble Board, tandis que le nombre de colonnes reste inchangé.
  3. Cliquez sur Apply layout. Le nombre devrait devenir Columns: 3, tandis que le titre choisi reste inchangé.
  4. Cliquez à nouveau sur l'un ou l'autre bouton. Il devrait appliquer à nouveau la même valeur synthétique.

Pour inspecter la frontière de type, changez temporairement la spécialisation du gestionnaire du premier bouton en CompletionAdapter<LayoutChoice> et donnez à sa fermeture un corps vide. Laissez la valeur de ce bouton comme TitleChoice. Le composant exige alors que des types incompatibles soient égaux via Handler.Value == Value, donc la compilation devrait échouer. Restaurez le gestionnaire original ensuite.

Si le compilateur signale une conformance répétée, cherchez une autre déclaration qui fait adopter CompletionHandling au même type nominal. Conservez la seule conformance d'adaptateur générique et créez une autre valeur d'adaptateur pour la sortie supplémentaire. Deux extensions contraintes déclarant la même conformance restent soumises à la restriction de conformance répétée de Swift. Conditional conformances — Multiple conformances.

Si le compilateur rejette la syntaxe de type associé principal du protocole, vérifiez la version du compilateur. Pour un compilateur plus ancien, le design d'adaptateur peut conserver une déclaration de type associé ordinaire à la place. Ceci est un fragment de remplacement pour la déclaration de protocole seulement ; gardez les autres déclarations inchangées :

@MainActor
protocol CompletionHandling {
    associatedtype Value
    func complete(_ value: Value)
}

Ce changement supprime la dépendance de cet exemple à la syntaxe Swift 5.7. Il ne supprime pas l'exigence du compilateur pour @MainActor, introduit dans Swift 5.5, ni ne change la cible de déploiement du tutoriel. Le composant utilise déjà une clause where générique, donc il n'a pas besoin de la syntaxe de type associé principal à son site d'utilisation. Lightweight same-type requirements, Global actors.

Qu'est-ce que ce contrat de complétion garantit ?

L'exemple garantit un type de charge utile correspondant à chaque frontière de composant. Son implémentation appelle le gestionnaire une fois par activation de bouton, de manière synchrone. Il n'implémente pas d'opération asynchrone, d'annulation, de persistance ou de cycle de vie de complétion unique. Ceux-ci nécessiteraient un comportement supplémentaire et un contrat explicite.

L'exemple utilise également des valeurs locales connues. Si un futur composant accepte des nombres de colonnes arbitraires, validez cette entrée avant de mettre à jour l'aperçu et définissez comment le rejet est signalé. Un type Swift correspondant seul n'établit pas qu'un entier est un nombre de colonnes utile.

Pour cette tâche de bureau, gardez un adaptateur par point de terminaison typé et laissez le parent posséder l'état résultant. Cela donne aux deux composants des contrats de complétion précis via une seule déclaration de conformance réutilisable, avec la destination de chaque résultat visible au site d'appel.

Sources