¿Es posible permitir que se llame a didSet durante la inicialización en Swift?


217

Pregunta

Los documentos de Apple especifican que:

Los observadores willSet y didSet no son llamados cuando una propiedad se inicializa por primera vez. Solo se llaman cuando el valor de la propiedad se establece fuera de un contexto de inicialización.

¿Es posible forzar que se invoquen estos durante la inicialización?

¿Por qué?

Digamos que tengo esta clase

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

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

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

Creé el método doStuffpara hacer que las llamadas de procesamiento sean más concisas, pero prefiero simplemente procesar la propiedad dentro de la didSetfunción. ¿Hay alguna manera de forzar esto a llamar durante la inicialización?

Actualizar

Decidí eliminar el intializador de conveniencia para mi clase y forzarlo a configurar la propiedad después de la inicialización. Esto me permite saber didSetque siempre se llamará. No he decidido si esto es mejor en general, pero se adapta bien a mi situación.


honestamente, es mejor "aceptar que así es como funciona Swift". si coloca un valor de inicialización en línea en la instrucción var, por supuesto, eso no llama "didSet". Es por eso que la situación "init ()" tampoco debe llamar a "didSet". todo tiene sentido.
Fattie

@Logan La pregunta misma respondió mi pregunta;) ¡gracias!
AamirR

2
Si desea utilizar el inicializador de conveniencia, puede combinarlo con defer:convenience init(someProperty: AnyObject) { self.init() defer { self.someProperty = someProperty }
Darek Cieśla

Respuestas:


100

Cree un método set propio y úselo dentro de su método init:

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

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

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

Al declarar somePropertycomo tipo: AnyObject!(una opción implícitamente desenvuelta), se permite que se inicialice completamente sin somePropertyestar configurado. Cuando llamas setSomeProperty(someProperty)estás llamando a un equivalente de self.setSomeProperty(someProperty). Normalmente no podría hacer esto porque self no se ha inicializado por completo. Como somePropertyno requiere inicialización y está llamando a un método dependiente de sí mismo, Swift deja el contexto de inicialización y didSet se ejecutará.


3
¿Por qué sería diferente de self.someProperty = newValue en el init? ¿Tienes un caso de prueba que funcione?
mmmmmm

2
No sé por qué funciona, pero lo hace. Establecer directamente el nuevo valor dentro de init () - El método no llama a didSet () - pero el uso del Método dado setSomeProperty () sí.
Oliver

50
Creo que sé lo que está pasando aquí. Al declarar somePropertycomo tipo: AnyObject!(una opción implícitamente desenvuelta), permite selfinicializar completamente sin somePropertyestar configurado. Cuando llamas setSomeProperty(someProperty)estás llamando a un equivalente de self.setSomeProperty(someProperty). Normalmente no podría hacer esto porque selfno se ha inicializado por completo. Como somePropertyno requiere inicialización y está llamando a un método dependiente self, Swift deja el contexto de inicialización y didSetse ejecutará.
Logan

3
Parece que todo lo que se configura fuera del init () REAL llama a didSet. Literalmente, todo lo que no esté configurado init() { *HERE* }habrá llamado a SetSet. Excelente para esta pregunta, pero el uso de una función secundaria para establecer algunos valores lleva a llamar a didSet. No es genial si quieres reutilizar esa función de configuración.
WCByrne

44
@ Mark en serio, tratar de aplicar el sentido común o la lógica a Swift es simplemente absurdo. Pero puedes confiar en los votos a favor o probarlo tú mismo. Lo acabo de hacer y funciona. Y sí, no tiene ningún sentido.
Dan Rosenstark

305

Si se utiliza deferdentro de un inicializador , para la actualización de las propiedades opcionales o más propiedades de actualización no opcionales que ya ha inicializado y después de que ha llamado a ningún super.init()métodos, a continuación, su willSet, didSetetc. será llamado. Creo que esto es más conveniente que implementar métodos separados que tiene que hacer un seguimiento de las llamadas en los lugares correctos.

Por ejemplo:

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

Rendirá:

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

66
Pequeño truco descarado, me gusta ... peculiaridad extraña de Swift.
Chris Hatton el

44
Excelente. Encontraste otro caso útil de uso defer. Gracias.
Ryan

1
Gran uso de aplazar! Ojalá pudiera darte más de un voto a favor.
Christian Schnorr

3
muy interesante .. Nunca había visto aplazar el uso de esta manera antes. Es elocuente y funciona.
aBikis

Pero configura myOptionalField dos veces, lo que no es excelente desde la perspectiva del rendimiento. ¿Qué pasa si se realizan cálculos pesados?
Yakiv Kovalskyi

77

Como una variación de la respuesta de Oliver, podría ajustar las líneas en un cierre. P.ej:

class Classy {

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

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

}

Editar: la respuesta de Brian Westphal es mejor en mi humilde opinión. Lo bueno de él es que insinúa la intención.


2
Eso es inteligente y no contamina la clase con métodos redundantes.
punto de

Gran respuesta, gracias! Vale la pena señalar que en Swift 2.2 debe envolver el cierre entre paréntesis siempre.
Max

@Max Cheers, la muestra incluye parens ahora
original_username

2
Respuesta inteligente! Una nota: si su clase es una subclase de otra cosa, deberá llamar a super.init () antes ({self.foo = foo}) ()
kcstricks el

1
@Hlung, no tengo la sensación de que es más probable que se rompa en el futuro que cualquier otra cosa, pero aún existe el problema de que sin comentar el código no está muy claro lo que hace. Al menos la respuesta de "aplazamiento" de Brian le da al lector una pista de que queremos realizar el código después de la inicialización.
original

10

Tuve el mismo problema y esto funciona para mí.

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

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

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

¿dónde lo obtuviste?
Vanya

3
Este enfoque ya no funciona. El compilador arroja dos errores: - 'self' usado en la llamada al método '$ defer' antes de que se inicialicen todas las propiedades almacenadas - Regreso del inicializador sin inicializar todas las propiedades almacenadas
Edward B

Tienes razón, pero hay una solución alternativa. Solo necesita declarar alguna propiedad como implícita sin envolver u opcional y proporcionarle un valor predeterminado con fusión nula para evitar fallas. tldr; someProperty debe ser un tipo opcional
Marque el

2

Esto funciona si haces esto en una subclase

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

Por aejemplo, didSetno se activa. Pero, por bejemplo, didSetse dispara, porque está en la subclase. Tiene que ver con lo que initialization contextrealmente significa, en este caso, eso superclasssí le importó


1

Si bien esto no es una solución, una forma alternativa de hacerlo sería usar un constructor de clases:

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

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

1
Esta solución requeriría que cambie la opcionalidad de, somePropertyya que no le habrá dado un valor durante la inicialización.
Mick MacCallum

1

En el caso particular en el que desea invocar willSeto didSetdentro initde una propiedad disponible en su superclase, simplemente puede asignar su súper propiedad directamente:

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

Tenga en cuenta que la solución de carlosismo con un cierre siempre funcionaría también en ese caso. Entonces mi solución es solo una alternativa.


-1

Puedes resolverlo de forma 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
    }
}

1
Hola @yshilov, no creo que esto realmente resuelva el problema que esperaba, ya que técnicamente, cuando llamas self.someProperty, estás dejando el alcance de inicialización. Creo que lo mismo podría lograrse haciendo var someProperty: AnyObject! = nil { didSet { ... } }. Aún así, algunos podrían apreciarlo como una solución, gracias por la adición
Logan el

@Logan no, eso no funcionará. didSet será completamente ignorado en init ().
yshilov
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.