Sometimes you need to modify your view controller to do a new thing. One thing leads to another, and you find yourself adding an optional variable property to the view controller, which will be set in some cases and not in others.
I think that more often than not, this is a flawed approach. There are a few reasons for this. First, a class with a optional property that is only used sometimes has a weak sense of identity. Put another way, adding optional properties blurs the fundamental meaning of the type. Second, this optional property doesn’t carry any semantic meaning. What does it mean about the state of this object if the type is nil? A casual reader of your code can’t tell in what cases the property will be nil, or what ramifications it has for the object. Third, code will continue to mutate. One optional property becomes two optional properties, which becomes three, and before you know it, you’ll end up at the bottom of a slippery slope. If we want to express that this value must exist if that value is nil, we can’t do that with simple optionals.
In all the codebases I’ve seen, I’ve found two primary reasons why you might need an optional property on your view controller. I’ll explore them both and suggest better patterns to solve each problem.
The first reason that you might have a problematic optional property is when you won’t necessarily have that object or piece of data every time this view controller is used.
One example of this that came up recently was a view controller that was being presented from a push notification in some cases. It needed to show a specific message that came from that notification. The simplest solution to this problem was to add an optional string property:
class LocationViewController: UIViewController {
//...
var notificationMessage: String?
//...
}
Other code in the view controller switched on the message’s existence to determine which views to allocate, how to lay them out, and so on. The optionality of the property represents more than existence of a simple string. It had implications in the rest of the view controller and the rest of the view layer.
The more essential point here is that the string now no longer represents a string that might be there or might not — it now represents the presentation style or mode that the view controller is in. Was it created in the context of a push notification or through normal browsing? The answer is in this barely related property!
To fix this problem, we have to make this mode fully explicit. Once it’s a first class citizen in this view controller, its effect on the view controller will be much more obvious.
class LocationViewController: UIViewController {
//...
enum Mode {
case fromNotification(message: String)
case normal
}
var mode: Mode
//...
}
As Sandi Metz says, there’s never one specialization. With an optional property, a sometimes-I-do-it-this-way, the code doesn’t admit that the nil
case of this property carries meaning as well and is really its own specialization. With an enum, this is reified and formalized in code.
Note that our new enum has a very similar shape to the enum that defines the Optional
type.
enum Optional<Wrapped> {
case some(Wrapped)
case none
}
It has a few extremely useful differences, though.
First, semantics. some
and none
are abstract; normal
and fromNotification
have meaning associated with them. Readers of your code will thank you.
Second, extensibility. If another mode is added to this view controller, we have a better means of fully describing it. If the new mode can never overlap with these two modes, we can add another enum case, with whatever necessary associated data. If that new mode can overlap with both of these current modes, it can be a new enum and a new property altogether. The state of the object is more readily describable. Either of these choices are better than adding yet another optional property for the new mode.
Third, extensibility again. Because LaunchViewController.Mode
is a first-class type, we can add functions and computed properties to it. For example, the height of the label that holds the notification message is probably dependent on the presence of the message itself. Therefore, we can move that code to the enum itself:
extension Mode {
var notificationLabelHeight: CGFloat {
switch self {
case .normal: return 0
case .fromNotification(let message): return message.size().height
}
}
}
Moving this data to a richer type gives you gains on all of these fronts, with very little code investment.
The second reason you might use an optional property is a subset of the first. In some cases, it’s not that the optional property represents a different mode of the view controller, it’s that it represents a temporal dimension to your code. You can’t initialize the property with a value because you don’t have it yet. Common examples of this are when you’re fetching something from the network, or waiting on some system resource that takes too long to retrieve and needs to be asynchronous.
class UserViewController: UIViewController {
//...
// will be loaded asynchronously
var user: User?
//...
}
This kind of problem can sometimes be papered over if the data comes back in array form. Since you can represent the non-existent state with the empty array, this problem may still be lurking, even if you’re not using an optional. The number of times that I’ve had a tough time adding empty states to a table view controller is a testament to this extent of this problem.
Since this problem is a subset of the first problem, we can solve it in the same way, with an enum:
enum LoadingState<Wrapped> {
case initial
case loading
case loaded(Wrapped)
case error(Error)
}
While this solution works, there is a better abstraction for managing asynchronous state. A value that doesn’t exist yet but will at some point in the future is best represented by a Promise
. Promises have several advantages, depending on your use case.
First, promises allow mutation, but only once and only from the pending state. Once a promise has been mutated (“fulfilled”), it can’t change again. (If you need something that can be updated multiple times, a Signal
or Observable
is what you’re looking for.) This means that you can have semantics that are similar to let
, but still manage asynchronous data.
Next, promises have facility for adding blocks that will get fired when a value comes in. If the property remained a simple optional property, these added blocks are equivalent to code in a didSet
property observer. However, promise blocks are more powerful, since more than one can be added, and they can be added on the fly from any place in the class. Further, if they’re added when the promise is already fulfilled with a value, they’ll immediately execute.
Lastly, if needed, promises handle errors as well. For certain kinds of asynchronous data, this is crucial, and you’ll get it for free.
If you’re using my Promise
library, you can create a pending Promise
with the empty initializer, and fulfill it at any time with the fulfill
method.
When it comes to optional properties, sometimes they reveal things about your code that may not have been apparent before. When you see yourself adding an optional parameter to a view controller, ask yourself, what does this optional really mean? Is there a better way to represent this data?