Pour afficher un tableau Foundation dans une fenêtre Mac, validez le type de ses éléments, puis créez une vue pour chaque valeur. Le pontage de NSArray vers [String] vous donne des chaînes ; pour les afficher, il faut ensuite les transformer séparément en étiquettes AppKit.

Ce guide s’adresse aux développeurs qui ajoutent une petite liste à leur propre app macOS. L’objectif concret est d’afficher trois noms fictifs — Circle, Square et Triangle — dans une pile verticale. L’exemple explicite les types de collection et d’éléments, la disposition et la durée de vie des objets.

Que change réellement le pontage d’une collection ?

Apple présente Array et NSArray comme les types Swift et Foundation correspondants, tout comme String et NSString. Apple recommande d’utiliser les types valeur de Swift dans le code Swift ordinaire et d’accéder aux types référence de Foundation lorsque leur comportement est nécessaire. Pour cette tâche, conservez la collection Foundation à la frontière d’entrée et utilisez ensuite un tableau Swift typé. Utiliser les types Foundation

Le type externe d’une collection et le type de ses éléments répondent à des questions différentes. NSArray désigne une collection ordonnée d’objets. [String] ajoute une exigence précise : chaque élément doit pouvoir être utilisé comme une chaîne Swift. Aucune de ces déclarations ne décrit une collection de vues. L’initialiseur de pile d’Apple exige explicitement [NSView]. NSArray, init(views:)

Étape Type dans cet exemple Contrat
Entrée Foundation NSArray Contient les objets fictifs fournis
Modèle validé [String] Chaque élément satisfait l’exigence relative aux chaînes
Objets de présentation [NSView] Chaque élément est une étiquette de texte nouvellement créée
Conteneur de disposition NSStackView Dispose ces vues selon sa configuration

Il est utile de distinguer le pontage de la construction. Le pontage relie des représentations compatibles, comme les chaînes Foundation et les chaînes Swift. Construire des objets NSTextField crée des objets de présentation dotés de leurs propres propriétés et durées de vie. Il n’existe pas de pont documenté entre une chaîne et une étiquette ; la méthode prise en charge consiste à appeler l’initialiseur d’étiquette pour chaque chaîne. Array, init(labelWithString:)

Comment valider les éléments sans en supprimer ?

Utilisez une conversion conditionnelle lorsque le contrat de type des éléments de la collection reçue doit être vérifié. Apple indique que as? produit un résultat facultatif et qu’une conversion forcée échouée avec as! déclenche une erreur d’exécution. Gérez explicitement l’échec avant de construire l’interface. Gérer les méthodes et objets de type dynamique dans Swift

Voici une vérification complète, limitée à Foundation, avec une entrée fictive. Enregistrez-la sous le nom BridgeCheck.swift et exécutez-la sur un Mac disposant des outils Swift avec swift BridgeCheck.swift. Ses assertions expriment les résultats attendus.

import Foundation

let source: NSArray = [
    NSString(string: "Circle"),
    NSString(string: "Square"),
    NSString(string: "Triangle")
]

let names = source as? [String]
assert(names == ["Circle", "Square", "Triangle"])

let mixed: NSArray = [NSString(string: "Circle"), NSNumber(value: 7)]
assert((mixed as? [String]) == nil)

let empty: NSArray = []
assert((empty as? [String]) == [])

print("Collection checks passed.")

La première conversion demande un tableau de chaînes ; elle vérifie donc le type requis des éléments lors de l’obtention du tableau typé. L’entrée mixte ne satisfait pas ce contrat. L’entrée vide le satisfait : elle ne contient aucun élément incompatible. Ces vérifications illustrent le contrat choisi pour l’entrée ; elles ne constituent ni des mesures ni des résultats d’application.

Évitez de remplacer cela par compactMap { $0 as? String } dans le seul but de faire passer une collection mixte. Apple définit compactMap comme une opération qui ne renvoie que les résultats de transformation qui ne sont pas nil. Dans cet exemple, cette approche supprimerait le nombre et afficherait une liste incomplète. Le filtrage peut convenir lorsqu’il est explicitement demandé ; ici, la tâche exige de conserver chaque élément fourni. compactMap(_:)

Une fois la validation réussie, utilisez map pour créer une étiquette par nom. Apple indique que map applique une transformation aux éléments de la séquence et que NSTextField(labelWithString:) crée une étiquette statique, sans retour à la ligne, dont le texte n’est pas sélectionnable. Cette transformation convient donc à ces trois noms courts. map(_:), init(labelWithString:)

Comment construire la fenêtre Mac complète ?

Cet exemple autonome nécessite macOS 14 ou une version ultérieure et les outils de ligne de commande Xcode installés. Il crée son interface par programmation ; aucun storyboard, fichier nib, ressource ni fichier source supplémentaire n’est nécessaire. La version minimale macOS 14 tient compte de l’utilisation de NSApplication.activate() dans l’exemple, qui demande l’activation sans la garantir. activate()

Enregistrez le programme complet ci-dessous sous le nom CollectionDemo.swift. Il inclut volontairement son propre point d’entrée. Pour l’adapter à une app existante, utilisez le point d’entrée et le cycle de vie déjà établis dans cette app, sans en ajouter un second.

import Foundation
import AppKit

@MainActor
final class DemoDelegate: NSObject, NSApplicationDelegate {
    var window: NSWindow?

    func applicationDidFinishLaunching(_ notification: Notification) {
        let source: NSArray = [
            NSString(string: "Circle"),
            NSString(string: "Square"),
            NSString(string: "Triangle")
        ]

        guard let names = source as? [String] else {
            print("Expected a collection containing only strings.")
            NSApplication.shared.terminate(nil)
            return
        }

        let labels: [NSView] = names.map { name in
            NSTextField(labelWithString: name)
        }

        let stack = NSStackView(views: labels)
        stack.orientation = .vertical
        stack.alignment = .leading
        stack.spacing = 8
        stack.translatesAutoresizingMaskIntoConstraints = false

        let content = NSView(frame: NSRect(
            x: 0, y: 0, width: 360, height: 180
        ))
        content.addSubview(stack)

        NSLayoutConstraint.activate([
            stack.leadingAnchor.constraint(
                equalTo: content.leadingAnchor, constant: 20
            ),
            stack.topAnchor.constraint(
                equalTo: content.topAnchor, constant: 20
            ),
            stack.trailingAnchor.constraint(
                lessThanOrEqualTo: content.trailingAnchor, constant: -20
            ),
            stack.bottomAnchor.constraint(
                lessThanOrEqualTo: content.bottomAnchor, constant: -20
            )
        ])

        let window = NSWindow(
            contentRect: content.frame,
            styleMask: [.titled, .closable, .miniaturizable],
            backing: .buffered,
            defer: false
        )
        window.title = "Synthetic Shapes"
        window.isReleasedWhenClosed = false
        window.contentView = content
        self.window = window
        window.center()
        window.makeKeyAndOrderFront(nil)
        NSApplication.shared.activate()
    }

    func applicationShouldTerminateAfterLastWindowClosed(
        _ sender: NSApplication
    ) -> Bool {
        true
    }
}

@main
@MainActor
enum CollectionDemo {
    static let appDelegate = DemoDelegate()

    static func main() {
        let app = NSApplication.shared
        app.setActivationPolicy(.regular)
        app.delegate = appDelegate
        app.run()
    }
}

Compilez-le et lancez-le depuis Terminal dans le répertoire qui contient le fichier :

xcrun swiftc -parse-as-library CollectionDemo.swift -o CollectionDemo
./CollectionDemo

Avec l’entrée fournie, le résultat attendu est une fenêtre intitulée Synthetic Shapes contenant les trois étiquettes dans leur ordre d’origine. La fermeture de la fenêtre demande l’arrêt de l’application. L’exemple renvoie true depuis la méthode du délégué qu’Apple prévoit à cet effet. applicationShouldTerminateAfterLastWindowClosed(_:)

Ces exemples ont été examinés, mais ni compilés ni exécutés ici : l’environnement de recherche ne fournit ni compilateur Swift ni AppKit macOS. Exécutez les vérifications Foundation et inspectez la fenêtre sur le Mac cible avant d’intégrer l’exemple.

Quels contrats de disposition et de cycle de vie faut-il respecter ?

Les valeurs par défaut documentées de l’initialiseur de pile sont importantes : il crée une pile horizontale, place les vues fournies dans sa zone de gravité principale et désactive la traduction du masque de redimensionnement automatique pour la pile. L’exemple définit explicitement l’orientation verticale et l’alignement à gauche. Une conversion de tableau ne peut choisir aucun de ces paramètres de disposition. init(views:), alignment

La pile gère la disposition des vues qu’elle contient. Les contraintes entre la pile et la vue de contenu qui l’englobe déterminent où cette disposition se trouve dans la fenêtre. Ici, les contraintes de gauche et du haut définissent une marge de 20 points ; les inégalités de droite et du bas réservent de l’espace sans obliger la courte liste à remplir la zone de contenu. Apple indique que les vues de pile utilisent Auto Layout et explique que la désactivation de la traduction du masque de redimensionnement automatique permet de fournir ses propres contraintes. NSStackView, translatesAutoresizingMaskIntoConstraints

Il s’agit d’une démonstration à taille fixe avec trois étiquettes courtes. Pour du texte plus long, un grand nombre de lignes ou une interface redimensionnable, il faut prendre une décision de disposition distincte — par exemple le retour à la ligne ou le défilement — avant d’étendre l’exemple. Le fait de réussir à ponter une collection plus grande ne garantit pas que sa présentation tiendra dans l’interface.

La durée de vie des objets est également indépendante de la conversion de type. Apple déclare faible la propriété de délégué de l’application. La propriété statique appDelegate fournit donc un détenteur fort explicite. Le délégué conserve aussi la fenêtre, et celle-ci définit isReleasedWhenClosed sur false, conformément aux recommandations d’Apple pour Swift et les clients ARC. delegate, isReleasedWhenClosed

La construction de l’interface a lieu dans applicationDidFinishLaunching, qu’Apple décrit comme un emplacement pour effectuer des initialisations supplémentaires après le démarrage de la boucle principale et avant le traitement des événements. Le point d’entrée du programme appelle run() pour démarrer cette boucle d’événements. Conservez ces responsabilités lorsque vous déplacez la conversion dans une app AppKit existante. applicationDidFinishLaunching(_:), run()

Les étiquettes représentent également un instantané construit. L’exemple définit le texte de chaque étiquette une seule fois. Si la collection source change par la suite, il faudra du code explicite pour mettre à jour les étiquettes ou reconstruire les vues disposées ; le pontage et l’opération de mappage n’établissent pas de lien permanent.

Comment repérer rapidement une confusion entre le conteneur et ses éléments ?

Suivez la séquence ci-dessous pour déterminer quel contrat a échoué. Elle sépare la validation des types des vérifications de présentation et de cycle de vie, afin qu’un problème de disposition ne vous pousse pas à réécrire un pontage qui fonctionne.

Symptôme Vérification Étape suivante prise en charge
Une conversion vers [String] renvoie nil Chaque élément d’entrée satisfait-il l’exigence relative aux chaînes ? Rejetez explicitement la collection ou définissez une règle de conversion intentionnelle.
Un tableau entier est converti en String La destination a-t-elle besoin d’une valeur ou d’une collection ? Validez [String] ; choisissez un élément uniquement si la tâche en nécessite réellement un.
Un initialiseur de pile rejette l’argument L’argument est-il de type [String] plutôt que [NSView] ? Convertissez d’abord chaque chaîne en étiquette avec map.
Les étiquettes apparaissent à l’horizontale L’orientation de la pile a-t-elle été définie ? Définissez .vertical pour cette liste.
Les vues sont mal placées ou les contraintes entrent en conflit Les contraintes de la vue englobante et les paramètres du masque de redimensionnement automatique sont-ils cohérents ? Vérifiez la disposition indépendamment de la conversion d’entrée.
Les rappels du délégué ne sont pas reçus Un détenteur fort conserve-t-il le délégué ? Conservez le délégué en vie au-delà de son affectation à la propriété faible.
Les changements ultérieurs du modèle ne s’affichent pas Existe-t-il du code de mise à jour explicite ? Mettez à jour les valeurs des étiquettes ou les vues disposées de façon délibérée.

Pour cette tâche, la méthode fiable consiste à valider une fois l’entrée Foundation, à conserver le modèle typé obtenu, à construire une étiquette par valeur et à configurer explicitement la pile et la durée de vie de l’application. Chaque étape a un contrat visible, ce qui facilite le diagnostic des problèmes à mesure que l’interface Mac évolue.

Sources