Add keyword to manually trigger didSet for instance vars set in init


(Daniel Roth) #1

Since the didSet method does not get called when the instance variable is set in initialization, it is common for developers to create helper methods that get called both in init method and in the didSet code block.

For example, consider this trivial example

class Example {
	var images: [UIImage] {
		didSet {
			imageViews = images.map({ UIImageView(image: $0 )})
		}
	}

	var imageViews: [UIImageViews]

	init(images: [UIImage]) {
		self.images = images
		self.imageViews = images.map({ UIImageView(image: $0 )})
	}
}

The generation of the imageViews array is duplicated in both the init and didSet methods. We could wrap this one line in a function call to ensure that any change to the logic gets propagated to both both places, but it would be so much simpler and more explicit if we could use a keyword to indicate that when we set self.images in the init method, we want it to trigger the didSet method as well.

I'm open to suggestions on what that keyword/syntax would look like.


(Charlie Monroe) #2

While I have run into similar issue a few times, it's easily solvable via lazy:

class Example {
    var images: [UIImage] {
        didSet {
            self.imageViews = self._recreateImageViews()
        }
    }

    
    lazy var imageViews: [UIImageView] = self._recreateImageViews()

    private func _recreateImageViews() -> [UIImageView] {
        return self.images.map(UIImageView.init(image:))
    }
    
     /// ...
}

(Josh Caswell) #3

I disagree that such a keyword would be more explicit. I think you're saying you want the keyword to go on the property observer. That would mean, reading init, you don't know which side effects might run or not without looking at the properties.

Possibly some shorthand that could be used inside init would be okay, though.


(Rob Mayoff) #4

Wrapping the property access inside a closure is enough to make Swift call the observers. So this would suffice:

{ self.images = images }()

But you can make it more self-documenting with a helper function:

func withPropertyObservers(do body: () throws -> ()) rethrows { try body() }

class Example {
    var images: [UIImage] = [] {
        didSet {
            print("didSet!")
            imageViews = images.map({ UIImageView(image: $0 )})
        }
    }

    var imageViews: [UIImageView] = []

    init(images: [UIImage]) {
        withPropertyObservers {
            self.images = images
        }
    }
}

(Daniel Roth) #5

Actually, I was thinking the keyword would be inside of init. So that reading it, it's easy to see that the line means "Set this initial value and call the setter". I realize that using lazy sort of solves the problem, but I take issue with the need for an additional method (in the above example, _recreateImageViews()).

Syntax along the lines of:

init(images: [UIImage]) {
    self.setImages(images)
}

would make it very clear (to me at least) that we are setting the images and explicitly calling the didSet method.


(Daniel Roth) #6

@mayoff , That's an interesting solution. I didn't realize that setting the value inside of a closure would work like that.


(Josh Caswell) #7

Ah, I misunderstood.