How to elegantly make sure you only setup your ViewController's UI on viewDidLoad
When handling navigation, we often end up having to pass some sort of state to our view controllers, wether directly or indirectly. Setting endless optional publicly exposed properties feels dirty and is a pain, while setting @IBOutlets before viewDidLoad is crash prone. Let's solve that together.
When handling navigation in iOS development, perhaps you programmatically instantiate your ViewControllers yourself, by hand (still my favorite). Perhaps you do it via a segue (to each his own ¯\_(ツ)_/¯).
Or maybe you do it programmatically, using Reusable (just like with RxFlow). Unless you do it one of the probably many other ways I know nothing of.
Still, there is an universal problem. That is, passing data from your current ViewController to the next.
One way would be to expose some or all of your View Controller's variables, have them set by the current view controller, coordinator, or whatever rocks your boat, then set everything up in viewDidLoad()
.
That's how it's done in the massively MVC app whose maintenance I inherited of as a freelance. Sometimes in some weird method coming from custom UIViewController subclass used everywhere. Sometimes in the prepareForSegue()
calls. It probably depends on the age of the code, I guess. But it's a mess.
Alternatively, you could expose a setup method in your ViewControllers, passing them the data they need, and have them setup everything. But then, if you use storyboards or Nibs, you risk setting @IBOutlets properties (ie, a label's text) before the viewDidLoad
call. In which case, they probably won't exist yet... and your app will crash. Damnit.
How do we solve that? We delay the UI's setup until the viewDidLoad()
call. And it's quite simple, really.
Overview (aka TL;DR)
- Define an optional
delayedSetup
function variable in your viewController - Hide away your variables. No viewController should be able to freely mingle with the internal state of another viewController. It's like walking around with your torso open. People don't do that (and live).
- Call it from your
viewDidLoad()
override (Pretty muchself.delayedSetup?()
) - Create your public or internal setup method(s) that will be called by external classes (named... say...
setup
? How's that for inspiration?) - Have these methods do all their UI setup work in an inline function
- Assign this inline function to
self.delayedSetup
- Tadaaa! Done! 🎉 🍾
Details with an example
Let's say we have an UIViewController
with an UIImageView
and an UILabel
, layed out in a .xib or a Storyboard. Like this:
class MyViewController: UIViewController {
@IBOutlet weak var label: UILabel!
@IBOutlet weak var imageView: UIImageView!
override func viewDidLoad() {
}
}
We define our optional delayedSetup function variable, and call it in our viewDidLoad
override like so:
class MyViewController: UIViewController {
@IBOutlet weak var label: UILabel!
@IBOutlet weak var imageView: UIImageView!
private var delayedSetup: (() -> ())? = nil
override func viewDidLoad() {
self.delayedSetup?()
}
}
Next we create our setup
method that the originating viewController, coordinator, viewModel or whatever will call to pass the label's text and the image view's image.
class MyViewController: UIViewController {
@IBOutlet weak var label: UILabel!
@IBOutlet weak var imageView: UIImageView!
private var delayedSetup: (() -> ())? = nil
override func viewDidLoad() {
self.delayedSetup?()
}
public func setup(with text: String, image: UIImage) {
}
}
And finally we create our delayedSetup
function and assign it to the delayedSetup
variable, doing the UI setup work in there
class MyViewController: UIViewController {
@IBOutlet weak var label: UILabel!
@IBOutlet weak var imageView: UIImageView!
private var delayedSetup: (() -> ())? = nil
override func viewDidLoad() {
self.delayedSetup?()
}
public func setup(with text: String, image: UIImage) {
self.delayedSetup = { [weak self] in
self?.label.text = text
self?.imageView.image = image
}
}
}
Going further
This is, obviously, a simple example. But the same approach applies in much more complex cases, with much more circumvoluted circumstances.
Want an example? Say different views may lead to the same view, passing along different (ie more or less complete / detailled) data models. You now have multiple different setup cases to handle.
struct CaptionnedPicture {
let id: String
let image: UIImage
let caption: String
}
class MyViewController: UIViewController {
@IBOutlet weak var label: UILabel!
@IBOutlet weak var imageView: UIImageView!
private var delayedSetup: (() -> ())? = nil
override func viewDidLoad() {
self.delayedSetup?()
}
public func setup(with text: String, image: UIImage) {
self.delayedSetup = { [weak self] in
self?.label.text = text
self?.imageView.image = image
}
}
public func setup(with imageId: String, picturesGateway: SomeNetworkingClass) {
self.delayedSetup = { [weak self] in
picturesGateway.fetchImage(id: imageId) { (url, caption) in
self?.label.text = caption
self?.imageView.setImage(from: url)
}
}
}
public func setup(with captionnedPicture: CaptionnedPicture) {
self.delayedSetup = { [weak self] in
self?.label.text = captionnedPicture.caption
self?.imageView.image = captionnedPicture.image
}
}
}
It's not much, but I hope it illustrates my point well enough. Refactored and simplified:
struct CaptionnedPicture {
let id: String
let image: UIImage
let caption: String
}
class MyViewController: UIViewController {
@IBOutlet weak var label: UILabel!
@IBOutlet weak var imageView: UIImageView!
private var delayedSetup: (() -> ())? = nil
override func viewDidLoad() {
self.delayedSetup?()
}
private func setup(caption: String, image: UIImage? = nil, imageUrl: URL? = nil) {
self.label.text = caption
if image != nil {
self.imageView.image = image
} else if imageUrl != nil {
self.imageView.setImage(from: imageUrl)
}
}
public func setup(with text: String, image: UIImage) {
self.delayedSetup = { [weak self] in
return self?.setup(caption: text, image: image)
}
}
public func setup(with imageId: String, picturesGateway: SomeNetworkingClass) {
self.delayedSetup = { [weak self] in
picturesGateway.fetchImage(id: imageId) { (url, caption) in
self?.setup(caption: caption, imageUrl: url)
}
}
}
public func setup(with captionnedPicture: CaptionnedPicture) {
self.delayedSetup = { [weak self] in
self?.setup(with: captionnedPicture.caption, image: captionnedPicture.image)
}
}
}
Conclusion
This definitely can be improved upon. But I hope you'll appreciate it as much as I do, that it will come in handy for you too, and that it doesn't turn out to be anti-pattern of some sort. If it is, or if you have any feedback, question, or a differing opinion on how it should be, please feel free to let me know on Twitter! (yes, I am too lazy to maintain and moderate my own comments feed)
Comments ()