3
votes

Singletons dans Swift et Interface Builder

Contexte

J'ai une classe singleton dans mon application, déclarée selon le singleton d'une ligne (avec un init () privé) dans ce billet de blog . Plus précisément, cela ressemble à ceci:

NSLog("aProperty: [\(singleton!.aProperty),\(String(describing:singleton!.value(forKey: "aProperty"))),\(Singleton.sharedInstance.singleton),\(String(describing:Singleton.sharedInstance.value(forKey: "aProperty")))] hidden: \(myMenuItem.isHidden)")

Je voudrais lier l'état de aProperty à savoir si un élément de menu est masqué.

Comment j'ai essayé de résoudre le problème

Voici les étapes que j'ai suivies pour ce faire:

  1. Accédez à la bibliothèque d'objets dans Interface Builder et ajoutez un "objet" générique à ma scène d'application. Dans l'inspecteur d'identité, configurez "Class" sur Singleton.

  2. Créez une prise de référence dans mon délégué d'application en faisant glisser la touche Ctrl de l'objet singleton dans Interface Builder vers le code de mon délégué d'application. Cela finit par ressembler à ceci:

singleton init
singleton init
sharedInstance = <MyModule.Singleton: 0x600000c616b0> singleton = Optional(<MyModule.Singleton: 0x600000c07330>)
  1. Accédez à l'inspecteur de liaisons pour l'élément de menu, choisissez "Caché" sous "Disponibilité", cochez "Lier à", sélectionnez "Singleton" dans la zone de liste déroulante en face de celui-ci et tapez aProperty code > sous "Chemin de la clé du modèle".

Le problème

Malheureusement, cela ne fonctionne pas: la modification de la propriété n'a aucun effet sur l'élément de menu en question.

Recherche de la cause h1 >

Le problème semble être que, malgré la déclaration de init () comme privé, Interface Builder parvient à créer une autre instance de mon singleton. Pour le prouver, j'ai ajouté NSLog ("singleton init") à la méthode privée init () ainsi que le code suivant à applicationDidFinishLaunching () code > dans mon délégué d'application:

NSLog("sharedInstance = \(Singleton.sharedInstance) singleton = \(singleton)")

Lorsque j'exécute l'application, ceci est affiché dans les journaux:

@IBOutlet weak var singleton: Singleton!

Par conséquent , il existe en effet deux cas différents. J'ai également ajouté ce code ailleurs dans mon délégué d'application:

@objc class Singleton {
    static let Singleton sharedInstance = Singleton()
    @objc dynamic var aProperty = false

    private init() {
    }
}

À un moment donné, cela produit la sortie suivante:

aProperty: [false, Optional (0), true, facultatif (1)] hidden: false

De toute évidence, étant un singleton, toutes les valeurs doivent correspondre, mais singleton produit une sortie et Singleton. sharedInstance en produit un autre. Comme on peut le voir, les appels à value (forKey :) correspondent à leurs objets respectifs, donc KVC ne devrait pas être un problème.

La question

Comment déclarer une classe singleton dans Swift et la câbler avec Interface Builder pour éviter qu'elle ne soit instanciée deux fois?

Si ce n'est pas possible, comment pourrais-je résoudre le problème de la liaison d'une propriété globale à un contrôle dans Interface Builder?

Un MCVE est-il nécessaire?

J'espère que la description était suffisamment détaillée, mais si quelqu'un pense qu'un MCVE est nécessaire, laissez un commentaire et je créerai un et téléverser sur GitHub.


5 commentaires

Même si vous dites que votre classe est un singleton, en ajoutant un nouvel Object au storyboard, vous ne faites pas référence à ce singleton. Vous créez une nouvelle instance . En fait, je suis presque sûr que vous ne pouvez pas faire cela. Vous ne pouvez pas référencer un objet créé dans le code dans vos storyboards.


@Sulthan Je pensais que la manière dont la classe était déclarée exclurait cela, car init () étant privé.


Interface Builder crée des objets à l'aide d'Objective-C et vous ne pouvez pas vraiment y appliquer un init privé. Même si vous le pouviez, vous obtiendriez juste une erreur lors du chargement du storyboard.


C'est dommage, je n'en avais aucune idée. Pouvez-vous suggérer un design différent dans ce cas? Peut-être que si je rendais une propriété statique ?


Cela fait quelques années, mais je pense que J'ai piraté des singletons de duplication IB en utilisant allocWithZone et copyWithZone


3 Réponses :


1
votes

Malheureusement, vous ne pouvez pas renvoyer une instance différente de init dans Swift. Voici quelques solutions de contournement possibles:

  • Créez une sortie pour une instance de votre classe dans Interface Builder, puis référencez uniquement cette instance dans tout votre code. (Ce n'est pas un singleton en soi, mais vous pouvez ajouter des vérifications d'exécution pour vous assurer qu'il n'est instancié qu'à partir d'un fichier nib et non à partir du code).
  • Créez une classe d'assistance à utiliser dans Interface Builder et exposez votre singleton en tant que propriété. C'est à dire. toute instance de cette classe d'assistance renverra toujours une seule instance de votre singleton.
  • Créez une sous-classe Objective-C de votre classe de singleton Swift et faites en sorte que ses init renvoient toujours une instance de singleton Swift partagée.

2 commentaires

Pour être honnête, vous pouvez renvoyer une instance différente de init dans Swift avec une solution de contournement laide mais autorisée, mais je l'éviterais car ce n'est pas une bonne idée. Et je considérerais cela malheureux. C'est toujours dommage de casser le système de type, même un peu.


Permettez-moi d'ajouter que j'ai utilisé l'option 2 (créer une classe d'assistance à utiliser dans Interface Builder) pour une classe différente, avec beaucoup de propriétés, où je ne voulais pas les lier manuellement selon ma réponse .



1
votes

Il existe un moyen de contourner le problème dans mon cas particulier.

Rappelez-vous de la question que je voulais uniquement cacher et afficher un menu en fonction de l'état de aProperty dans ce singleton. Alors que j'essayais d'éviter d'écrire autant de code que possible, en faisant tout dans Interface Builder, il semble que dans ce cas, il soit beaucoup moins compliqué d'écrire simplement la liaison par programme:

menuItem.bind(NSBindingName.hidden, to: Singleton.sharedInstance, withKeyPath: "aProperty", options: nil)

p>


0 commentaires

4
votes

Je veux juste commencer ma réponse en déclarant que les singletons ne doivent pas être utilisés pour partager l'état global. Bien qu'ils puissent sembler plus faciles à utiliser au début, ils ont tendance à générer beaucoup de maux de tête par la suite, car ils peuvent être modifiés pratiquement de n'importe où, rendant parfois votre programme imprévisible.

Cela étant dit, ce n'est pas impossible à réaliser ce dont vous avez besoin, mais avec un peu de cérémonie:

@objc class Singleton: NSObject {
    // using this class behind the scenes, this is the actual singleton
    class SingletonStorage: NSObject {
        @objc dynamic var aProperty = false
    }
    private static var storage = SingletonStorage()

    // making sure all instances use the same storage, regardless how
    // they were created
    @objc dynamic var storage = Singleton.storage

    // we need to tell to KVO which changes in related properties affect
    // the ones we're interested into
    override class func keyPathsForValuesAffectingValue(forKey key: String) -> Set<String> {
        switch key {
        case "aProperty":
            return ["storage.aProperty"]
        default: return super.keyPathsForValuesAffectingValue(forKey: key)
        }

    }

    // and simply convert it to a computed property
    @objc dynamic var aProperty: Bool {
        get { return Singleton.storage.aProperty }
        set { Singleton.storage.aProperty = newValue }
    }
}


4 commentaires

La propriété en question vérifie si j'ai pu me connecter à un outil d'assistance, et je pense que c'est une bonne utilisation du modèle singleton. Il n'est mis à jour que par le code à l'intérieur de la classe, il ne peut pas être défini par un autre code. Cela dit, il semble qu'au lieu d'essayer de forcer IB à accepter un singleton, mon autre réponse finit par être la plus claire solution dans ce cas particulier, à mon avis.


Ce serait parfait si vous supprimez tout après la première ligne! ;-) Dans tous les cas, votre classe Singleton est vraiment mal nommée, car ce n'est pas du tout un singleton. Vous pouvez l'appeler SingletonProxy ou FauxSingleton ou quelque chose comme ça.


@Caleb Je suis d'accord, j'ai juste utilisé les mêmes noms que dans la question. BTW, j'aurais pu ajouter une propriété shared et rendre le init privé, les laisser à l'extérieur pour ne pas encombrer le code de la solution.


@swineone ne peut pas discuter de la propreté, après tout, la solution que vous avez trouvée n'a qu'une seule ligne;)