270

Question

Apple's docs specify that:

willSet and didSet observers are not called when a property is first initialized. They are only called when the property’s value is set outside of an initialization context.

Is it possible to force these to be called during initialization?

Why?

Let's say I have this class

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

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

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

I created the method doStuff, to make the processing calls more concise, but I'd rather just process the property within the didSet function. Is there a way to force this to call during initialization?

Update

I decided to just remove the convenience intializer for my class and force you to set the property after initialization. This allows me to know didSet will always be called. I haven't decided if this is better overall, but it suits my situation well.

Slipp D. Thompson
  • 33,165
  • 3
  • 43
  • 43
Logan
  • 52,262
  • 20
  • 99
  • 128
  • 1
    it is honestly best just to "accept this is how swift works". if you put an inline initialization value on the var statement, of course that does not call "didSet". that's why the "init()" situation must also not call "didSet". it all makes sense. – Fattie Oct 29 '17 at 16:41
  • @Logan The question itself answered my question ;) thanks! – AamirR Jun 28 '18 at 10:15
  • 2
    If you want to use the convenience initializer, you can combine it with `defer`: `convenience init(someProperty: AnyObject) { self.init() defer { self.someProperty = someProperty } ` – Darek Cieśla Oct 25 '18 at 06:11

9 Answers9

376

If you use defer inside of an initializer, for updating any optional properties or further updating non-optional properties that you've already initialized and after you've called any super.init() methods, then your willSet, didSet, etc. will be called. I find this to be more convenient than implementing separate methods that you have to keep track of calling in the right places.

For example:

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

Will yield:

I'm going to change to 3.14
Now I'm 3.14
Paulo Mattos
  • 18,845
  • 10
  • 77
  • 85
Brian Westphal
  • 6,058
  • 4
  • 24
  • 20
125

Create an own set-Method and use it within your init-Method:

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

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

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

By declaring someProperty as type: AnyObject! (an implicitly unwrapped optional), you allow self to fully initialize without someProperty being set. When you call setSomeProperty(someProperty) you're calling an equivalent of self.setSomeProperty(someProperty). Normally you wouldn't be able to do this because self hasn't been fully initialized. Since someProperty doesn't require initialization and you are calling a method dependent on self, Swift leaves the initialization context and didSet will run.

Nike Kov
  • 12,630
  • 8
  • 75
  • 122
Oliver
  • 1,458
  • 1
  • 12
  • 12
  • 3
    Why would this differ from self.someProperty = newValue in the init? Have you got a working test case? – mmmmmm Aug 10 '14 at 17:12
  • 2
    Don't know why it works -- but it does. Directly setting the new value within the init()-Method doesn't call didSet() -- but using the given Method setSomeProperty() does. – Oliver Aug 10 '14 at 17:17
  • 53
    I think I know what's happening here. By declaring `someProperty` as type: `AnyObject!` (an implicitly unwrapped optional), you allow `self` to fully initialize without `someProperty` being set. When you call `setSomeProperty(someProperty)` you're calling an equivalent of `self.setSomeProperty(someProperty)`. Normally you wouldn't be able to do this because `self` hasn't been fully initialized. Since `someProperty` doesn't require initialization and you are calling a method dependent on `self`, Swift leaves the initialization context and `didSet` will run. – Logan Aug 10 '14 at 17:47
  • 3
    It looks like anything that is set outside of the actual init() DOES get didSet called. Literally anything that is not set `init() { *HERE* }` will have didSet called. Great for this question but using a secondary function to set some values leads to didSet being called. Not great if you want to reuse that setter function. – WCByrne Mar 14 '15 at 02:39
  • 4
    @Mark seriously, trying to apply common sense or logic to Swift is just absurd. But you can trust the upvotes or try it yourself. I just did and it works. And yes, it makes no sense at all. – Dan Rosenstark Apr 08 '16 at 01:34
  • This isn't horrible. Since the `setSomeProperty()` method is only used as a work-around for what can't be done in `init()`, you could just as well name it `lateInit()` or `start()` or `begin()` or `ignition()` and just use that for any post-member-value-initialization stuff you want to run when the object is created. – Slipp D. Thompson Jun 27 '16 at 05:32
  • @Logan is correct. You can test out his theory for yourself by trying to set a non-optional property after calling the method. It won't work since you need to initialize all properties before you can use self. – Dan Loewenherz Jul 15 '16 at 13:32
  • @Logan Your observation is *almost* correct. Whenever you have `var x : String?` then automatically `x` **is** set to `nil` (same **isn't** true if you used `let` instead of `var`). Same is true for `var x : String!`. The only difference is this time if you access *when* it's `nil` you will crash. If you don't access it, it doesn't mean that it's value isn't set. It means it's `nil` but you're not messing with it to create an error. For more see [here](https://stackoverflow.com/questions/27797351/class-has-no-initializers-swift/40769917#40769917). Make sure you see the links at the end. – mfaani Jul 18 '17 at 19:36
  • @Logan You can always 'initialize' a class with implicitly unwrapped optionals. What you can't do is access them safely. You can only access them after it's value is changed from `nil` to a non-nil value. FWIW it having a value of `nil` doesn't mean it's not set. It **is** set, but set to `nil`. – mfaani Jul 18 '17 at 19:45
  • This doesn't seem to work in latest version of Xcode and Swift – Adithya Jun 26 '19 at 08:01
85

As a variation of Oliver's answer, you could wrap the lines in a closure. Eg:

class Classy {

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

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

}

Edit: Brian Westphal's answer is nicer imho. The nice thing about his is that it hints at the intent.

original_username
  • 2,398
  • 20
  • 24
  • 2
    That's clever and does not pollute the class with redundant methods. – pointum Sep 03 '15 at 22:59
  • Great answer, thanks! Worth noting that in Swift 2.2 you need to wrap the closure in parentheses always. – Max Mar 24 '16 at 18:19
  • @Max Cheers, the sample include parens now – original_username Mar 29 '16 at 02:06
  • 3
    Clever answer! One note: if your class is a subclass of something else, you will need to call super.init() before ({ self.foo = foo })() – kcstricks Apr 06 '16 at 20:47
  • 1
    @Hlung, I don't get a feeling it's more likely to break in future than anything else, but there is still the problem that without commenting the code it's not very clear what it does. At least Brian's "defer" answer gives the reader a hint that we want to perform the code after initialization. – original_username Apr 28 '16 at 21:05
11

I had the same problem and this works for me

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

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

    func doStuff() {
        // do stuff now that someProperty is set
    }
}
Nicolas Miari
  • 16,006
  • 8
  • 81
  • 189
Carmine Cuofano
  • 191
  • 1
  • 3
  • where did you get it? – Vanya Oct 28 '17 at 02:26
  • 4
    This approach no longer works. The compiler throws two errors: – 'self' used in method call '$defer' before all stored properties are initialized – Return from initializer without initializing all stored properties – Edward B May 10 '18 at 18:35
  • 1
    You're right but there's a workaround. You only need to declare someProperty as implicit unwrapped or optional and supply it will a default value with nil coalescing to avoid failures. tldr; someProperty must be an optional type – Mark Apr 10 '19 at 03:18
2

This works if you do this in a subclass

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

In a example, didSet is not triggered. But in b example, didSet is triggered, because it is in the subclass. It has to do something with what initialization context really means, in this case the superclass did care about that

onmyway133
  • 45,645
  • 31
  • 257
  • 263
  • Hi @onmyway133 swift being a type safe language always fully initialized a type else not. When you initialized someClass. Base class being fully initialized and then in SomeClass you are setting someProperty = "hello". Hence didSet being called – Roshan Sah Oct 14 '20 at 06:19
1

While this isn't a solution, an alternative way of going about it would be using a class constructor:

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

    class func createInstance(someProperty: AnyObject) -> SomeClass {
        let instance = SomeClass() 
        instance.someProperty = someProperty
        return instance
    }  
}
Dan Loewenherz
  • 10,879
  • 7
  • 50
  • 81
Snowman
  • 31,411
  • 46
  • 180
  • 303
  • 1
    This solution would require you to change the optionality of `someProperty` since it won't have given it a value during initialization. – Mick MacCallum Jan 31 '18 at 14:30
1

In the particular case where you want to invoke willSet or didSet inside init for a property available in your superclass, you can simply assign your super property directly:

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

Note that Charlesism solution with a closure would always work too in that case. So my solution is just an alternative.

Community
  • 1
  • 1
Cœur
  • 37,241
  • 25
  • 195
  • 267
1

unfortunately, didSet observers aren't called when a root class is initialized.

If your class isn't a subclass, you have to use getters and setters to achieve the functionality you want:

class SomeClass {
    private var _test: Int = 0
    var test: Int {
        get { _test }
        set { _test = newValue }
    }
    
    init(test: Int) { self.test = test }
}

alternatively, if your class is a subclass, you can use didSet and do:

override init(test: int) {
    super.init()
    self.test = test
}

The didSet SHOULD get called after super.init() is called.

One thing I have not tried but MIGHT also work:

init(test: int) {
    defer { self.test = test }
}

NOTE: you will need to make your properties nullable, or set a default value for them, or unwrap the class properties.

Xaxxus
  • 1,083
  • 12
  • 19
-1

You can solve it in obj-с way:

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
  • 774
  • 1
  • 8
  • 11
  • 1
    Hi @yshilov, I don't think this really solves the problem I was hoping to since technically, when you call `self.someProperty` you're leaving initialization scope. I believe the same thing could be accomplished by doing `var someProperty: AnyObject! = nil { didSet { ... } }`. Still, some might appreciate it as a work around, thanks for the addition – Logan Sep 02 '15 at 18:16
  • @Logan nope, that will not work. didSet will be completely ignored in init(). – yshilov Sep 02 '15 at 19:11