É possível permitir que didSet seja chamado durante a inicialização no Swift?

217

Questão

Os documentos da Apple especificam que:

Os observadores willSet e didSet não são chamados quando uma propriedade é inicializada. Eles são chamados apenas quando o valor da propriedade é definido fora de um contexto de inicialização.

É possível forçar que estes sejam chamados durante a inicialização?

Por quê?

Digamos que eu tenho essa aula

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

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

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

Criei o método doStuff, para tornar as chamadas de processamento mais concisas, mas prefiro apenas processar a propriedade dentro da didSetfunção. Existe uma maneira de forçar isso a chamar durante a inicialização?

Atualizar

Decidi remover o intializer de conveniência da minha classe e forçá-lo a definir a propriedade após a inicialização. Isso me permite saber didSetque sempre será chamado. Ainda não decidi se isso é melhor no geral, mas combina bem com minha situação.

Logan
fonte
honestamente, é melhor apenas "aceitar que é assim que funciona rápido". se você colocar um valor de inicialização embutido na instrução var, é claro que não chama "didSet". é por isso que a situação "init ()" também não deve chamar "didSet". tudo faz sentido.
Fattie
@Logan A própria pergunta respondeu à minha pergunta;) obrigado!
AamirR
2
Se você deseja usar o inicializador de conveniência, pode combiná-lo com defer:convenience init(someProperty: AnyObject) { self.init() defer { self.someProperty = someProperty }
Darek Cieśla

Respostas:

100

Crie um método próprio e use-o dentro do seu método init:

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

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

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

Ao declarar somePropertycomo tipo: AnyObject!(um opcional implicitamente desembrulhado), você se inicializa totalmente sem somePropertyser definido. Quando você liga, setSomeProperty(someProperty)liga para o equivalente a self.setSomeProperty(someProperty). Normalmente você não seria capaz de fazer isso porque o self não foi totalmente inicializado. Como somePropertynão requer inicialização e você está chamando um método dependente de si, Swift sai do contexto de inicialização e o didSet será executado.

Oliver
fonte
3
Por que isso difere de self.someProperty = newValue no init? Você tem um caso de teste de trabalho?
precisa saber é
2
Não sei por que funciona - mas funciona. Definir diretamente o novo valor dentro do init () - O método não chama didSet () -, mas o método fornecido setSomeProperty () fornece.
Oliver
50
Eu acho que sei o que está acontecendo aqui. Ao declarar somePropertycomo tipo: AnyObject!(um opcional implicitamente desembrulhado), você selfpode inicializar completamente sem somePropertyser definido. Quando você liga, setSomeProperty(someProperty)liga para o equivalente a self.setSomeProperty(someProperty). Normalmente você não seria capaz de fazer isso porque selfnão foi totalmente inicializado. Como somePropertynão requer inicialização e você está chamando um método dependente self, Swift sai do contexto de inicialização e didSetserá executado.
Logan
3
Parece que qualquer coisa que esteja definida fora do init () real é chamada de didSet. Literalmente, qualquer coisa que não esteja definida init() { *HERE* }terá o didSet chamado. Ótimo para esta pergunta, mas o uso de uma função secundária para definir alguns valores leva ao chamado didSet. Não é ótimo se você deseja reutilizar essa função de incubadora.
WCByrne
4
@ Mark a sério, tentar aplicar bom senso ou lógica a Swift é simplesmente absurdo. Mas você pode confiar nos votos positivos ou tentar você mesmo. Acabei de fazer e funciona. E sim, não faz nenhum sentido.
Dan Rosenstark
305

Se você usar deferdentro de um inicializador , para atualizar as propriedades opcionais ou ainda atualizar as propriedades não-opcionais que você já tenha inicializado e depois de você ter chamado quaisquer super.init()métodos, então o seu willSet, didSet, etc. será chamado. Acho isso mais conveniente do que implementar métodos separados, dos quais você precisa acompanhar as chamadas nos lugares certos.

Por exemplo:

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

Produzirá:

I'm going to change to 3.14
Now I'm 3.14
Brian Westphal
fonte
6
Truque insolente, eu gosto ... estranho esquisito de Swift.
Chris Hatton
4
Ótimo. Você encontrou outro caso útil de uso defer. Obrigado.
22716 Ryan
1
Grande uso de adiar! Gostaria de poder lhe dar mais de um voto positivo.
22417 Christian Schnorr
3
muito interessante .. Eu nunca vi adiar usado assim antes. É eloquente e funciona.
aBikis
Mas você define myOptionalField duas vezes, o que não é ótimo da perspectiva de desempenho. E se forem feitos cálculos pesados?
Yakiv Kovalskyi
77

Como uma variação da resposta de Oliver, você pode encerrar as linhas em um fechamento. Por exemplo:

class Classy {

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

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

}

Edit: A resposta de Brian Westphal é melhor imho. O bom dele é que ele indica a intenção.

original_username
fonte
2
Isso é inteligente e não polui a classe com métodos redundantes.
pointum 3/09/15
Ótima resposta, obrigado! Vale ressaltar que no Swift 2.2 você precisa envolver o fechamento entre parênteses sempre.
Max
@ Max Cheers, a amostra inclui parens agora
original_username
2
Resposta inteligente! Uma nota: se sua classe é uma subclasse de outra coisa, você vai precisar ligar para super.init () antes de ({self.foo = foo}) ()
kcstricks
1
@Hlung, eu não tenho a sensação de que é mais provável que ocorra no futuro do que qualquer outra coisa, mas ainda existe o problema de que, sem comentar o código, não está muito claro o que ele faz. Pelo menos a resposta "adiada" de Brian dá ao leitor uma dica de que queremos executar o código após a inicialização.
original_username
10

Eu tive o mesmo problema e isso funciona para mim

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
fonte
onde você conseguiu isso?
Vanya #
3
Essa abordagem não funciona mais. O compilador lança dois erros: - 'self' usado na chamada de método '$ adiar' antes de todas as propriedades armazenadas serem inicializadas - Retornar do inicializador sem inicializar todas as propriedades armazenadas
Edward B
Você está certo, mas há uma solução alternativa. Você só precisa declarar alguma propriedade como implícita desembrulhada ou opcional e fornecer um valor padrão com coalescência nula para evitar falhas. tldr; someProperty deve ser um tipo opcional
Mark
2

Isso funciona se você fizer isso em uma subclasse

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

No aexemplo, didSetnão é acionado. Mas, por bexemplo, didSeté acionado, porque está na subclasse. Tem a ver com o que initialization contextrealmente significa, neste caso, o superclassque importava com isso

onmyway133
fonte
1

Embora isso não seja uma solução, uma maneira alternativa de fazer isso seria usar um construtor de classe:

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

    class func createInstance(someProperty: AnyObject) -> SomeClass {
        let instance = SomeClass() 
        instance.someProperty = someProperty
        return instance
    }  
}
Boneco de neve
fonte
1
Essa solução exigiria que você alterasse a opcionalidade, somePropertypois ela não terá dado um valor durante a inicialização.
Mick MacCallum
1

No caso específico em que você deseja chamar willSetou didSetentrar initpara uma propriedade disponível em sua superclasse, você pode simplesmente atribuir sua super propriedade diretamente:

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

Observe que a solução do Charlesismo com um fechamento sempre funcionaria também nesse caso. Então, minha solução é apenas uma alternativa.

Cœur
fonte
-1

Você pode resolvê-lo de maneira objetiva:

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
fonte
1
Oi @yshilov, acho que isso realmente não resolve o problema que eu esperava, desde que tecnicamente, quando você ligar, self.somePropertysair do escopo de inicialização. Eu acredito que a mesma coisa poderia ser realizada fazendo var someProperty: AnyObject! = nil { didSet { ... } }. Ainda assim, alguns podem apreciá-lo como um em torno do trabalho, graças a adição
Logan
@Logan não, isso não vai funcionar. didSet será completamente ignorado em init ().
yshilov 02/09/2015