assignTo

Not 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 Ts 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.