assignTo
09 May 2024Not sure if this is a good idea or a terrible one but here we go… It’s not uncommon to find yourself in situations where you need to add little bits of glue code to handle the optionality of instance variables at the point you construct their values.
Here’s an example where I need to assign an optional instance variable and also return the non optional result from the function that is doing the creation.
class Coordinator {
private var controller: MyViewController?
func start() -> MyViewController {
controller = MyViewController()
return controller!
}
}
In the above it’s tempting to just use the instance variable but then to fulfil the contract we end up having to force unwrap.
We can improve this by creating a local let
that is non optional, which can then be assigned to the optional instance variable
class Coordinator {
private var controller: MyViewController?
func start() -> MyViewController {
let controller = MyViewController()
self.controller = controller
return controller
}
}
This works and removes the less than desirable handling of the optionality but it introduces the risk that we might forget to assign the instance variable.
In practice you see many different slight variations of this glue code e.g.
class Coordinator {
private var controller: MyViewController?
func start0() -> MyViewController {
let controller = MyViewController()
self.controller = controller
return controller
}
// Making the contract weaker by returning the optional
func start1() -> MyViewController? {
controller = MyViewController()
return controller
}
// Being more explicit and explaining the unwrapping
func start2() -> MyViewController {
controller = MyViewController()
guard let controller else {
fatalError("We should never get here")
}
return controller
}
}
Of the above I think start0
is the most desirable as it models the optionality correctly and doesn’t resort to any unwrapping, it’s just unfortunate that it’s 3 lines and you can forget the assignment.
The above are simplified examples and in real code the functions will likely be longer so this juggling of optionality could be spread out or just be less clear. This isn’t just limited to cases where you need to assign and return there are lots of times where you need to create an object, assign it to an instance variable and perform some further configuration; one common example would be
func loadView() {
view = UIView()
view?.backgroundColor = .red
view?.alpha = 0.5
}
Here I’m having to handle the safe unwrapping ?
multiple times.
I could use if let
/guard let
but it starts getting wordy again.
Let’s create a helper
Here’s an idea for a helper that can get us back to a single line of code in the simplest case and help avoid the additional unwraps in the loadView
case.
By adding these helpers
func assignTo<T>(_ to: inout T?, _ value: T) -> T {
to = value
return value
}
@_disfavoredOverload
func assignTo<T>(_ to: inout T, _ value: T) -> T {
to = value
return value
}
We can now simplify the original start example to
func start() -> MyViewController {
assignTo(&controller, .init())
}
and the loadView
example loses all the optional handling
func loadView() {
let view = assignTo(&view, UIView())
view.backgroundColor = .red
view.alpha = 0.5
}
@_disfavoredOverload
I could be wrong but my understanding that for my usage I expect the value and return to be non optional.
The to
parameter could be optional or non optional depending on requirements.
Without the annotation Swift will try to make all the T
s align as Optional<Wrapped>
which is not what I wanted.
With the annotation Swift is now clever enough to know that it should really only have one optional and not try and force all T
positions to be Optional.
Wrap up
No idea if this is a good idea or not but I took some inspiration from Combine having assign(to:)
and assign(to:on:)
methods.
A little more
The types do have to line up which can make things less ideal e.g. in the loadView
example if I needed to configure things available to a subclass of UIView
then I’m back to needing to do some reassignment and typecasting e.g.
func loadView() {
guard let view = assignTo(&view, MyView()) as? MyView else { return }
view.title = "title"
}
You can kind of work around this by composing other helper methods - a common one in Swift projects is with
which would look like this
func loadView() {
let view = assignTo(&view, with(MyView()) { $0.title = "title" })
registerView(view)
}
In the above it’s only inside the scope of the with
trailing closure that we know it’s an instance of MyView
and I had to add another function call registerView
that takes the view to make it worthwhile needing to use assignTo
still.