iOS: The One Weird Trick For Testing View Controllers in Swift

I’ve been testing View Controllers in Swift for the past year, but for some reason, last week all my tests would just randomly start failing due to some type of timer / race condition issues. Turns out, I was setting up my View Controller tests incorrectly:

The problem was clearly with the last line in my setup NSRunLoop.mainRunLoop().runUntilDate(NSDate()) . Since the goal here is to make sure that the UIViewController life cycle functions – including viewDidLoad and viewDidAppear – get called correctly, I replaced this line with viewController.loadView(). I ran into the same frustrating issue.

So after googling around, I found an amazing presentation from the testing expert @modocache: Everything You (N)ever Wanted to Know about Testing View Controllers . Go ahead and flip though it. I’ll wait!

Instead of using NSRunLoop.mainRunLoop().runUntilDate(NSDate()) or viewController.loadView(), the key is to use let _ = viewController.view like this:

I’ll be honest and be the first to admit that this is not a solution I would have naturally come up with ever. After discussing it with @allonsykraken, I kind of understand why it works.

The key here is that Apple overrides the viewController’s view getter to call the loadView function and do a bunch of other things we have no access to. If anyone else has other great insights into why this works, feel free to add it in the comments!

I went ahead and updated my other blog posts on View Controller testing if you’re interested in learning more:

UPDATE: As Tomasz Szulc points out below in the comments, Apple has a whole explanation in the documentation about why you shouldn’t call loadView():

You should never call this method directly. The view controller calls this method when its view property is requested but is currently nil. This method loads or creates a view and assigns it to the view property.

If the view controller has an associated nib file, this method loads the view from the nib file. A view controller has an associated nib file if the nibName property returns a non-nil value, which occurs if the view controller was instantiated from a storyboard, if you explicitly assigned it a nib file using the initWithNibName:bundle: method, or if iOS finds a nib file in the app bundle with a name based on the view controller’€™s class name. If the view controller does not have an associated nib file, this method creates a plain UIView object instead.

If you use Interface Builder to create your views and initialize the view controller, you must not override this method.

You can override this method in order to create your views manually. If you choose to do so, assign the root view of your view hierarchy to the view property. The views you create should be unique instances and should not be shared with any other view controller object. Your custom implementation of this method should not call super.

If you want to perform any additional initialization of your views, do so in the viewDidLoad method.

UPDATE 2: I really like @salutis‘s suggestion on making this a lot more readable:

Here is what the code would look like:

UPDATE 3: (This is the reason I love blogging. Learning so much right now!) Another great Swift tip from @jckarter that I didn’t know about!

So your extension can now be even prettier like this:

UPDATE 4: @joemasilotti has a less hacky solution to loading the view!

Enjoy the article? Join over 14,500+ Swift developers and enthusiasts who get my weekly updates.

  • no variable “let _ = ” declaration needed, to call loadView()

    • ketzusaka

      She said in the post she tried that and it didn’t do what was needed

  • You tried with loadView(). I just wanted to say that you should never call loadView() method directly. This is ugly hack. Apple does not recommend it.

    I think accessing view property is the best solution because it starts entire flow of loading view from storyboard (actually from nib file that is created by Xcode internally) and configuring view including setting constraints and calling viewWillAppear and viewDidAppear of view controller by default.

    • Thanks! Didn’t realize they said this in the documentation. Will add as an update.

  • Note that iOS 9 introduces loadViewIfNeeded() on UIViewController. The documentation is pretty minimal on that for now, but it looks like that’s OK to call directly.

    • For testing, I want to make sure the view is loaded – it’s mandatory for all my tests. Do you know if loadViewIfNeeded() will load it in the testing scenario I discuss above?

      • Natasha, I see loadViewIfNeeded() do the same calls as accessing view directly. Accessing view does not call loadViewIfNeeded() but both accessing view and loadViewIfNeeded() call first loadViewIfRequired(), so it should be safe to call loadViewIfNeeded() then.

      • I think loadViewIfNeeded() was created so that you don’t have to use .view to just load the view, even if you didn’t need it. Objective-C has a warning for that (“using getter as side effect” or something along those lines), so this is a cleaner way of ensuring the view is loaded.

    • Could you share some link with documentation about the method? I cannot find any. Or minimal means that there is no documentation at all? :>

      • Minimal in this case means there is a comment in the header file that says “Loads the view controller’s view if it has not already been set.”

        That’s all I could find on it.

  • I’ve always relied on using the begin/end appearance transitions to ensure all the hierarchy + view controller set up is done correctly.

    • It does the same thing like accessing view or loadViewIfNeeded() but from different side. It calls viewWillAppear(_:) first and then loadViewIfRequired(). IMO loadViewIfNeeded() is the best approach for now. The loadViewIfNeeded() calls viewWillAppear(_:) later in its flow.

      • I appreciate the feedback, but I see nothing in the iOS9 documentation about this function. The appearance methods are the way that apple have been recommending that we deal with the viewDid/WillXXX functions in their docs. This really is the way to ensure you get it set up like you would in you app.

        • I’ve double checked it and you’re right. your approach might be better in testing.

          I’ve run the code in applicationDidFinishLaunchingWithOptions method and the flow was almost the same for every way I loaded the view. When I moved the code into tests and do not assigned the view controller to any window it behaves differently.

          – Accessing view and calling loadViewIfNeeded() finished its flow on viewDidLoad() method.
          – Your approach allowed the view controller to call private __viewWillAppear() method at the beginning of the flow and then view was loaded and set and viewDidLoad(), viewWillAppear() and viewDidAppear() have been called. So your flow is more complete than the previous in testing.
          Thanks for this!

        • Mouhcine El Amine

          Hey @orta:disqus ,
          Thanks for the great tip! (the link is outdated as you changed the file names)
          Are there some docs from Apple except from the “beginAppearancetransition” documentation in UIViewController class reference?

  • Pingback: iOS: UIViewController’s view loading process demystified : Tomasz Szulc()

  • James

    Hi Natasha, thanks for the informative post. I had come across similar issues before aswell and here were the results of my findings.

  • Thank for blog
    I try repeat test viewController but have error. How Can I fix that?

    • Любомир Маринов

      Is your Storyboard initial view controller of type UINavigationController? Because the explicitly unwrapped variable seems to return ‘nil’. Try unwrapping with ‘if let …’ and check if the code in the closure executes.

      p.s. 9 months later ….. damn, I feel like a spammer 😀

  • Ethan Sherr

    “Apple hates developer who came up with this One Weird Trick to test view controllers in swift” Hah! Thanks for the post.

  • Walt
  • I have always used this trick to properly trigger lifecyle of viewControllers. But I actually never added the controller to UIWindow. #learntSomethingToday #Thanks