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.

How to elegantly make sure you only setup your ViewController's UI on viewDidLoad

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.

Boy, did someone take that trash out.
Segue navigation is a mess. Do Apple engineers really use that? - Photo by NeONBRAND / Unsplash

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)

  1. Define an optional delayedSetup function variable in your viewController
  2. 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).
  3. Call it from your viewDidLoad() override (Pretty much self.delayedSetup?())
  4. Create your public or internal setup method(s) that will be called by external classes (named... say... setup? How's that for inspiration?)
  5. Have these methods do all their UI setup work in an inline function
  6. Assign this inline function to self.delayedSetup
  7. Tadaaa! Done! 🎉 🍾

Details with an example

Let's take a closer look - Photo by morais / Unsplash

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)