È possibile consentire a didSet di essere chiamato durante l'inizializzazione in Swift?


217

Domanda

I documenti di Apple specificano che:

Gli osservatori willSet e didSet non vengono chiamati quando una proprietà viene inizializzata per la prima volta. Vengono chiamati solo quando il valore della proprietà è impostato al di fuori di un contesto di inizializzazione.

È possibile forzare la loro chiamata durante l'inizializzazione?

Perché?

Diciamo che ho questa lezione

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

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

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

Ho creato il metodo doStuff, per rendere le chiamate di elaborazione più concise, ma preferirei semplicemente elaborare la proprietà all'interno della didSetfunzione. C'è un modo per forzare questo a chiamare durante l'inizializzazione?

Aggiornare

Ho deciso di rimuovere semplicemente l'intializer di convenienza per la mia classe e costringerti a impostare la proprietà dopo l'inizializzazione. Questo mi permette di sapere che didSetverrà sempre chiamato. Non ho deciso se nel complesso sia meglio, ma si adatta bene alla mia situazione.


onestamente è meglio solo "accettare questo è come funziona rapidamente". se si inserisce un valore di inizializzazione incorporato nell'istruzione var, ovviamente questo non chiama "didSet". ecco perché anche la situazione "init ()" non deve chiamare "didSet". tutto ha un senso.
Fattie,

@Logan La domanda stessa ha risposto alla mia domanda;) grazie!
AamirR,

2
Se si desidera utilizzare l'inizializzatore di convenienza, è possibile combinarlo con defer:convenience init(someProperty: AnyObject) { self.init() defer { self.someProperty = someProperty }
Darek Cieśla,

Risposte:


100

Crea un tuo set-Method e usalo nel tuo init-Method:

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

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

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

Dichiarando somePropertycome tipo: AnyObject!(un'opzione implicitamente non scartata), si consente a sé di inizializzare completamente senza somePropertyessere impostato. Quando chiami setSomeProperty(someProperty), chiami un equivalente di self.setSomeProperty(someProperty). Normalmente non saresti in grado di farlo perché il sé non è stato completamente inizializzato. Poiché somePropertynon richiede l'inizializzazione e stai chiamando un metodo dipendente da sé, Swift lascia il contesto di inizializzazione e didSet verrà eseguito.


3
Perché questo differirebbe da self.someProperty = newValue nell'init? Hai un caso di test funzionante?
mmmmmm

2
Non so perché funzioni, ma funziona. Impostando direttamente il nuovo valore all'interno di init () - Il metodo non chiama didSet () - ma usando il metodo dato setSomeProperty () lo fa.
Oliver,

50
Penso di sapere cosa sta succedendo qui. Dichiarando somePropertycome tipo: AnyObject!(un'opzione implicitamente da scartare), si consente selfdi inizializzare completamente senza somePropertyessere impostato. Quando chiami setSomeProperty(someProperty), chiami un equivalente di self.setSomeProperty(someProperty). Normalmente non saresti in grado di farlo perché selfnon è stato completamente inizializzato. Poiché somePropertynon richiede l'inizializzazione e stai chiamando un metodo dipendente self, Swift lascia il contesto di inizializzazione e didSetverrà eseguito.
Logan,

3
Sembra che tutto ciò che è impostato al di fuori dell'attuale init () venga chiamato. Letteralmente tutto ciò che non è stato impostato init() { *HERE* }avrà chiamato. Ottimo per questa domanda, ma l'utilizzo di una funzione secondaria per impostare alcuni valori porta alla chiamata di didSet. Non eccezionale se si desidera riutilizzare quella funzione setter.
WCByrne

4
@Mark seriamente, cercare di applicare il buon senso o la logica a Swift è semplicemente assurdo. Ma puoi fidarti dei voti o provarlo tu stesso. L'ho appena fatto e funziona. E sì, non ha alcun senso.
Dan Rosenstark,

305

Se si utilizza deferall'interno di un inizializzatore , per aggiornare le proprietà opzionali o ulteriore aggiornamento proprietà non opzionali che hai già inizializzato e dopo aver chiamato tutti i super.init()metodi, allora il vostro willSet, didSete così via si chiamerà. Trovo che questo sia più conveniente dell'implementazione di metodi separati che devi tenere traccia delle chiamate nei posti giusti.

Per esempio:

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
        }
    }
}

Produrrà:

I'm going to change to 3.14
Now I'm 3.14

6
Piccolo trucco sfacciato, mi piace ... strana stranezza di Swift però.
Chris Hatton,

4
Grande. Hai trovato un altro utile caso di utilizzo defer. Grazie.
Ryan,

1
Ottimo uso del differimento! Vorrei poterti dare più di un voto.
Christian Schnorr,

3
molto interessante .. non ho mai visto il rinvio usato così prima d'ora. È eloquente e funziona.
aBikis il

Ma hai impostato myOptionalField due volte, il che non è eccezionale dal punto di vista delle prestazioni. Cosa succede se vengono effettuati calcoli pesanti?
Yakiv Kovalskyi,

77

Come variante della risposta di Oliver, potresti avvolgere le linee in una chiusura. Per esempio:

class Classy {

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

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

}

Modifica: la risposta di Brian Westphal è più bella. La cosa bella della sua è che accenna all'intenzione.


2
È intelligente e non inquina la classe con metodi ridondanti.
pointum,

Ottima risposta, grazie! Vale la pena notare che in Swift 2.2 è necessario avvolgere sempre la chiusura tra parentesi.
Max

@Max Cheers, l'esempio include ora le parentesi
originale

2
Risposta intelligente! Una nota: se la tua classe è una sottoclasse di qualcos'altro, dovrai chiamare super.init () prima ({self.foo = foo}) ()
kcstricks il

1
@Hlung, non ho la sensazione che possa rompersi in futuro più di ogni altra cosa, ma c'è ancora il problema che senza commentare il codice non è molto chiaro cosa faccia. Almeno la risposta "differita" di Brian dà al lettore un suggerimento che vogliamo eseguire il codice dopo l'inizializzazione.
originale

10

Ho avuto lo stesso problema e questo funziona per me

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

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

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

dove l'hai preso?
Vanya,

3
Questo approccio non funziona più. Il compilatore genera due errori: - 'self' utilizzato nella chiamata del metodo '$ defer' prima che tutte le proprietà memorizzate siano inizializzate - Ritorno dall'inizializzatore senza inizializzare tutte le proprietà memorizzate
Edward B

Hai ragione ma c'è una soluzione alternativa. Devi solo dichiarare someProperty come implicito da scartare o facoltativo e fornirlo avrà un valore predefinito con coalescenza nulla per evitare guasti. TLDR; someProperty deve essere un tipo opzionale
Segna il

2

Funziona se lo fai in una sottoclasse

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()

Ad aesempio, didSetnon viene attivato. Ma ad besempio, didSetviene attivato perché si trova nella sottoclasse. Deve fare qualcosa con ciò che initialization contextrealmente significa, in questo caso superclassse ne preoccupava


1

Anche se questa non è una soluzione, un modo alternativo per farlo sarebbe usare un costruttore di classi:

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

    class func createInstance(someProperty: AnyObject) -> SomeClass {
        let instance = SomeClass() 
        instance.someProperty = someProperty
        return instance
    }  
}

1
Questa soluzione richiederebbe di modificare l'opzione di somePropertypoiché non gli avrebbe dato un valore durante l'inizializzazione.
Mick MacCallum

1

Nel caso particolare in cui desideri invocare willSeto didSetall'interno initdi una proprietà disponibile nella tua superclasse, puoi semplicemente assegnare direttamente la tua super proprietà:

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

Si noti che la soluzione di Charlesism con una chiusura funzionerebbe sempre anche in quel caso. Quindi la mia soluzione è solo un'alternativa.


-1

Puoi risolverlo in modo oggettivo:

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
    }
}

1
Ciao @yshilov, non penso che questo risolva davvero il problema che speravo da quando tecnicamente, quando chiami self.somePropertystai lasciando l'ambito di inizializzazione. Credo che la stessa cosa possa essere realizzata facendo var someProperty: AnyObject! = nil { didSet { ... } }. Tuttavia, alcuni potrebbero apprezzarlo come soluzione, grazie per l'aggiunta
Logan

@Logan no, non funzionerà. didSet verrà completamente ignorato in init ().
yshilov,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.