A lazy write-once Swift setter
An interesting post on Natasha the Robot's blog about data injection with storyboards and segues in swift got me to thinking. I'm using the same pattern as described in the post to set my data sources and other gubbins in prepareForSegue(_:sender:), and it seems
- Pretty unavoidable if you want to use segues as they're designed
- Uncomfortably permissive
What do I mean about uncomfortably permissive. Well, because all view controllers are created in a storyboard using an init(_: NSCoder) we can't use custom initialisers for our view controllers. This in turn means our dependencies are, by necessity, vars instead of lets and there is nothing stopping someone from accidentally resetting these vars at some point.
A custom initialiser would fix this, we could use let everywhere and the world would be a happier place, but like I said, we can't have this. I suggested some form of lazy-let variable, one that doesn't need to be set in the initialiser, but once written to, will not allow further writes. But what would that look like?
Let's start with a class that doesn't have a customer initialiser, and uses the same pattern as defined in Natasha's post.
So far, so normal. But I don't want to expose the variable in order to protect it from being written to multiple times.
Not only that, do we want it to be available for reading? Probably not. So forget the fascination with trying to make variables the be all and end all, let's just make the variable private and use an old fashioned setter.
Ok, so at this point we have achieved precisely nothing other than increasing the lines of code. We've hidden access to the variable, but that's about it. We can still set the variable multiple times. Let's fix that now.
We create a generic setter in UIViewController that will only set if the variable has not yet been initialised.
And now we can use this generic setting where necessary for any variables we want lazily instantiated.
We can see the first call to set the variable succeeds, but the second one throws.
It would have been nice if we could have put this in the original variable's willSet{} block, but that can't throw, so unfortunately we're reduced to creating a setter for every private variable. This isn't ideal, but it gets the job done.
There are three major downsides to this solution
- It's still a runtime check, the beauty of true lets is the compiler will stop you from making a logical mistake, but we don't have that luxury with this kind of lazy-let pattern.
- This stops 3rd parties from accidentally setting the variable more than once. It doesn't, however, stop the view controller from resetting it. But hey, if you're going to do that, you deserve the problems you're bringing on yourself.
- What? I have to create a variable and write a setter? What is this? Java?