Est-il possible d'autoriser didSet à être appelé lors de l'initialisation dans Swift?

217

Question

Les documents d'Apple spécifient que:

Les observateurs willSet et didSet ne sont pas appelés lors de la première initialisation d'une propriété. Ils sont uniquement appelés lorsque la valeur de la propriété est définie en dehors d'un contexte d'initialisation.

Est-il possible de forcer ces appels lors de l'initialisation?

Pourquoi?

Disons que j'ai cette classe

class SomeClass {
    var someProperty: AnyObject {
        didSet {
            doStuff()
        }
    }

    init(someProperty: AnyObject) {
        self.someProperty = someProperty
        doStuff()
    }

    func doStuff() {
        // do stuff now that someProperty is set
    }
}

J'ai créé la méthode doStuffpour rendre les appels de traitement plus concis, mais je préfère simplement traiter la propriété dans la didSetfonction. Existe-t-il un moyen de forcer cet appel lors de l'initialisation?

Mettre à jour

J'ai décidé de supprimer l'intialiseur de commodité pour ma classe et de vous forcer à définir la propriété après l'initialisation. Cela me permet de savoir didSetsera toujours appelé. Je n'ai pas décidé si c'est mieux dans l'ensemble, mais cela convient bien à ma situation.

Logan
la source
honnêtement, il vaut mieux "accepter que c'est comme ça que ça marche". si vous mettez une valeur d'initialisation en ligne sur l'instruction var, bien sûr, cela n'appelle pas "didSet". c'est pourquoi la situation "init ()" ne doit pas non plus appeler "didSet". tout devient clair.
Fattie
@Logan La question elle-même a répondu à ma question;) merci!
AamirR
2
Si vous souhaitez utiliser l'initialiseur de commodité, vous pouvez le combiner avec defer:convenience init(someProperty: AnyObject) { self.init() defer { self.someProperty = someProperty }
Darek Cieśla

Réponses:

100

Créez une propre méthode set et utilisez-la dans votre méthode init:

class SomeClass {
    var someProperty: AnyObject! {
        didSet {
            //do some Stuff
        }
    }

    init(someProperty: AnyObject) {
        setSomeProperty(someProperty)
    }

    func setSomeProperty(newValue:AnyObject) {
        self.someProperty = newValue
    }
}

En déclarant somePropertycomme type: AnyObject!(une option implicitement déballée), vous autorisez self à s'initialiser complètement sans somePropertyêtre défini. Lorsque vous appelez, setSomeProperty(someProperty)vous appelez un équivalent de self.setSomeProperty(someProperty). Normalement, vous ne seriez pas en mesure de le faire car self n'a pas été complètement initialisé. Puisque somePropertyne nécessite pas d'initialisation et que vous appelez une méthode dépendante de vous-même, Swift quitte le contexte d'initialisation et didSet s'exécutera.

Oliver
la source
3
Pourquoi cela différerait-il de self.someProperty = newValue dans l'init? Avez-vous un cas de test fonctionnel?
mmmmmm
2
Je ne sais pas pourquoi cela fonctionne - mais c'est le cas. Définition directe de la nouvelle valeur dans init () - La méthode n'appelle pas didSet () - mais en utilisant la méthode donnée setSomeProperty () le fait.
Oliver
50
Je pense que je sais ce qui se passe ici. En déclarant en somePropertytant que type: AnyObject!(une option implicitement déballée), vous autorisez l' selfinitialisation complète sans somePropertyêtre définie. Lorsque vous appelez, setSomeProperty(someProperty)vous appelez un équivalent de self.setSomeProperty(someProperty). Normalement, vous ne pourrez pas le faire car il selfn'a pas été complètement initialisé. Puisque somePropertyne nécessite pas d'initialisation et que vous appelez une méthode dépendante de self, Swift quitte le contexte d'initialisation et didSets'exécute.
Logan
3
Il ressemble à tout ce qui est défini en dehors du véritable init () DOIT faire didSet appelé. Littéralement, tout ce qui n'est pas défini init() { *HERE* }aura appelé didSet. Idéal pour cette question, mais l'utilisation d'une fonction secondaire pour définir certaines valeurs conduit à l'appel de didSet. Pas génial si vous voulez réutiliser cette fonction de setter.
WCByrne
4
@Mark sérieusement, essayer d'appliquer le bon sens ou la logique à Swift est tout simplement absurde. Mais vous pouvez faire confiance aux votes positifs ou l'essayer vous-même. Je viens de le faire et ça marche. Et oui, cela n'a aucun sens.
Dan Rosenstark
305

Si vous utilisez l' deferintérieur d'un initialiseur , pour mettre à jour des propriétés facultatives ou pour mettre à jour des propriétés non facultatives que vous avez déjà initialisées et après avoir appelé des super.init()méthodes, votre willSet,didSet etc. sera appelée. Je trouve cela plus pratique que d'implémenter des méthodes distinctes que vous devez suivre pour appeler aux bons endroits.

Par exemple:

public class MyNewType: NSObject {

    public var myRequiredField:Int

    public var myOptionalField:Float? {
        willSet {
            if let newValue = newValue {
                print("I'm going to change to \(newValue)")
            }
        }
        didSet {
            if let myOptionalField = self.myOptionalField {
                print("Now I'm \(myOptionalField)")
            }
        }
    }

    override public init() {
        self.myRequiredField = 1

        super.init()

        // Non-defered
        self.myOptionalField = 6.28

        // Defered
        defer {
            self.myOptionalField = 3.14
        }
    }
}

Donnera:

I'm going to change to 3.14
Now I'm 3.14
Brian Westphal
la source
6
Petit tour effronté, j'aime ça ... bizarrerie bizarre de Swift cependant.
Chris Hatton
4
Génial. Vous avez trouvé un autre cas utile d'utilisation defer. Merci.
Ryan
1
Grande utilisation du report! J'aimerais pouvoir vous donner plus d'un vote positif.
Christian Schnorr
3
très intéressant .. Je n'ai jamais vu de report utilisé comme ça auparavant. C'est éloquent et ça marche.
aBikis
Mais vous définissez myOptionalField deux fois, ce qui n'est pas génial du point de vue des performances. Et si des calculs lourds avaient lieu?
Yakiv Kovalskyi
77

Comme variante de la réponse d'Oliver, vous pouvez envelopper les lignes dans une fermeture. Par exemple:

class Classy {

    var foo: Int! { didSet { doStuff() } }

    init( foo: Int ) {
        // closure invokes didSet
        ({ self.foo = foo })()
    }

}

Edit: la réponse de Brian Westphal est à mon humble avis. La bonne chose à propos du sien est qu'il fait allusion à l'intention.

nom_utilisateur_original
la source
2
C'est intelligent et ne pollue pas la classe avec des méthodes redondantes.
pointum
Excellente réponse, merci! À noter que dans Swift 2.2, vous devez toujours mettre la fermeture entre parenthèses.
Max
@Max Cheers, l'exemple inclut des parens maintenant
original_username
2
Réponse intelligente! Une remarque: si votre classe est une sous-classe d'autre chose, vous devrez appeler super.init () avant ({self.foo = foo}) ()
kcstricks
1
@Hlung, je n'ai pas l'impression qu'il est plus susceptible de se casser à l'avenir qu'autre chose, mais il y a toujours le problème que sans commenter le code, ce n'est pas très clair. Au moins, la réponse "différer" de Brian donne au lecteur une indication que nous voulons exécuter le code après l'initialisation.
original_username
10

J'ai eu le même problème et cela fonctionne pour moi

class SomeClass {
    var someProperty: AnyObject {
        didSet {
            doStuff()
        }
    }

    init(someProperty: AnyObject) {
        defer { self.someProperty = someProperty }
    }

    func doStuff() {
        // do stuff now that someProperty is set
    }
}
Carmine Cuofano
la source
où l'avez-vous obtenu?
Vanya
3
Cette approche ne fonctionne plus. Le compilateur génère deux erreurs: - 'self' utilisé dans l'appel de méthode '$ defer' avant que toutes les propriétés stockées ne soient initialisées - Retour de l'initialiseur sans initialiser toutes les propriétés stockées
Edward B
Vous avez raison, mais il existe une solution de contournement. Il vous suffit de déclarer someProperty comme déballé implicite ou facultatif et de lui fournir une valeur par défaut avec coalescence nulle pour éviter les échecs. tldr; someProperty doit être un type facultatif
Mark
2

Cela fonctionne si vous le faites dans une sous-classe

class Base {

  var someProperty: AnyObject {
    didSet {
      doStuff()
    }
  }

  required init() {
    someProperty = "hello"
  }

  func doStuff() {
    print(someProperty)
  }
}

class SomeClass: Base {

  required init() {
    super.init()

    someProperty = "hello"
  }
}

let a = Base()
let b = SomeClass()

Par aexemple, didSetn'est pas déclenché. Mais dans l' bexemple, didSetest déclenché, car il est dans la sous-classe. Il doit faire quelque chose avec ce initialization contextqui signifie vraiment, dans ce cas, superclassil s'en est occupé

onmyway133
la source
1

Bien que ce ne soit pas une solution, une autre façon de procéder serait d'utiliser un constructeur de classe:

class SomeClass {
    var someProperty: AnyObject {
        didSet {
            // do stuff
        }
    }

    class func createInstance(someProperty: AnyObject) -> SomeClass {
        let instance = SomeClass() 
        instance.someProperty = someProperty
        return instance
    }  
}
Bonhomme de neige
la source
1
Cette solution vous obligerait à modifier le caractère optionnel de somePropertycar il ne lui aura pas donné de valeur lors de l'initialisation.
Mick MacCallum
1

Dans le cas particulier où vous souhaitez invoquer willSetou à l' didSetintérieur initpour une propriété disponible dans votre superclasse, vous pouvez simplement assigner directement votre super propriété:

override init(frame: CGRect) {
    super.init(frame: frame)
    // this will call `willSet` and `didSet`
    someProperty = super.someProperty
}

Notez que la solution de Charlesisme avec une fermeture fonctionnerait toujours aussi dans ce cas. Ma solution n'est donc qu'une alternative.

Cœur
la source
-1

Vous pouvez le résoudre de manière obj-с:

class SomeClass {
    private var _someProperty: AnyObject!
    var someProperty: AnyObject{
        get{
            return _someProperty
        }
        set{
            _someProperty = newValue
            doStuff()
        }
    }
    init(someProperty: AnyObject) {
        self.someProperty = someProperty
        doStuff()
    }

    func doStuff() {
        // do stuff now that someProperty is set
    }
}
yshilov
la source
1
Salut @yshilov, je ne pense pas que cela résout vraiment le problème que j'espérais depuis techniquement, lorsque vous appelez, self.somePropertyvous quittez la portée d'initialisation. Je crois que la même chose pourrait être accomplie en faisant var someProperty: AnyObject! = nil { didSet { ... } }. Pourtant, certains pourraient l'apprécier comme solution de contournement, merci pour l'ajout
Logan
@Logan non, cela ne fonctionnera pas. didSet sera complètement ignoré dans init ().
yshilov