This episode is freely available thanks to the support of our subscribers

Subscribers get exclusive access to new and all previous subscriber-only episodes, video downloads, and 30% discount for team members. Become a Subscriber

We explore different approaches to factor out asynchronous loading code from view controllers, using protocols, container view controllers, and generics.

00:07 Let's continue talking about loading data from the network. We've covered the networking part already, so today we'll talk about ways to handle asynchronous requests in the UI. When loading data from the network, we always face the same problem: we don't have the data yet, but we want to show the user some kind of activity or progress indicator. Once the data arrives, we want to configure the views.

00:41 We start by implementing this pattern inside of a single view controller. Afterward, we'll look at different approaches of how we can factor out the loading logic.

Making View Controllers Asynchronous

00:57 The starting point is a simple view controller, EpisodeDetailViewController, which simply displays the title of one episode. 01:08 In viewDidLoad, we set the background color and add the label as a subview:

final class EpisodeDetailViewController: UIViewController {
    let titleLabel = UILabel()
    
    convenience init(episode: Episode) {
        self.init()
        titleLabel.text = episode.title
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .whiteColor()
        
        titleLabel.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(titleLabel)
        titleLabel.constrainEdges(toMarginOf: view)
    }
}

01:17 For the loading part, we add another initializer to the view controller. This initializer takes a Resource<Episode> instead of just an Episode. 01:34 In the Networking episode, we talked about the networking part and how we work with resources. 01:42 Within this initializer, we use the sharedWebservice to load the data for the resource that gets passed in. 01:50 Once the network request is finished, we get a callback with a result. 02:00 If we can't extract an episode from this result, we just return immediately, and we use a guard statement for this early exit. 02:21 After the guard, we know that we have an episode and we can update the title label. Since we're referencing self when we access the title label, we add self as weak to the callback's capture list, because we don't want to reference the view controller unnecessarily. After all, it might not be onscreen anymore once the network call comes back. 02:46 Lastly, we also need to call the super class's initializer to make this work:

convenience init(resource: Resource<Episode>) {
    self.init()
    sharedWebservice.load(resource) { [weak self] result in
        guard let value = result.value else { return } // TODO loading error
        self?.titleLabel.text = value.title
    }
}

02:59 With this initializer in place, we can update the call site. Instead of handing an episode, we hand a resource to the view controller:

let episodesVC = EpisodeDetailViewController(resource: episodeResource)

03:33 We're still missing an activity indicator. 03:41 We'll add a spinner property — which holds an instance of UIActivityIndicatorView — to the view controller. We start the spinner right before we make the network call. 03:57 Once the network request is finished, we stop the spinner, regardless of whether or not the request succeeded. Again, we reference spinner with self? in order to not capture the view controller strongly:

convenience init(resource: Resource<Episode>) {
    self.init()
    spinner.startAnimating()
    sharedWebservice.load(resource) { [weak self] result in
        self?.spinner.stopAnimating()
        guard let value = result.value else { return } // TODO loading error
        self?.titleLabel.text = value.title
    }
}

04:06 We configure the spinner in viewDidLoad. 04:16 We set hidesWhenStopped to true, disable the resizing mask translation, and add the spinner as a subview. 04:38 Finally, we center it in the current view using one of our convenience auto layout extensions:

spinner.hidesWhenStopped = true
spinner.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(spinner)
spinner.center(inView: view)

05:01 To make the spinner show up, we still need to initialize it with a particular style. For now, we'll simply use the .Gray style:

let spinner = UIActivityIndicatorView(activityIndicatorStyle: .Gray)

05:26 That was a lot of boilerplate code to make a basic activity indicator work, and we'll have to type this over and over again in lots of different view controllers. 05:39 As such, it would be much better to factor this logic out of the view controller. Doing so would save us quite a bit of work, and it would make the view controller simpler as well.

Creating the Loading Protocol

05:57 The first approach we can try is to create a protocol and pull the code we wrote into a protocol extension. This approach is very similar to what Apple demoed in the WWDC 2015 session for Protocol-Oriented Programming. 06:25 We start by adding a Loading protocol, which defines the spinner as a read-only property. 06:47 We also add a load method — which performs the actual network call and starts and stops the spinner — to the protocol. This method takes a resource as parameter. Since we can't specify the generic parameter of Resource any further, we add an associatedtype called ResourceType to the protocol so that we can work with resources of any type:

protocol Loading {
    associatedtype ResourceType
    var spinner: UIActivityIndicatorView { get }
    func load(resource: Resource<ResourceType>)
}

07:08 Since we want to provide an implementation for the load method, we create a protocol extension. 07:21 Actually, it's sufficient to only declare the load method in the protocol extension, because we don't want this method to be overwritten by the classes that conform to the Loading protocol:

protocol Loading {
    associatedtype ResourceType
    var spinner: UIActivityIndicatorView { get }
}

extension Loading {
    func load(resource: Resource<ResourceType>) {
        // TODO
    }
}

07:36 Now we just move the code that we wrote in the view controller's initializer into the protocol's load method. 07:52 The method still performs the same tasks: starting the spinner, loading the data from the web service, and stopping the spinner. To make the code work, we constrain the protocol extension to instances of UIViewController:

extension Loading where Self: UIViewController {
    func load(resource: Resource<ResourceType>) {
        spinner.startAnimating()
        sharedWebservice.load(resource) { [weak self] result in
            self?.spinner.stopAnimating()
            guard let value = result.value else { return } // TODO loading error
            // TODO configure views
        }
    }
}

08:18 In our protocol extension, we don't know how to configure the views after we get the data back from the network. As such, we have to delegate this task back to the view controller itself. 08:33 For this, we add a configure method to the protocol that takes a value of type ResourceType. 08:47 We call configure once the data comes back from the network:

protocol Loading {
    // ...
    func configure(value: ResourceType)
}

extension Loading where Self: UIViewController {
    func load(resource: Resource<ResourceType>) {
        spinner.startAnimating()
        sharedWebservice.load(resource) { [weak self] result in
            self?.spinner.stopAnimating()
            guard let value = result.value else { return } // TODO loading error
            self?.configure(value)
        }
    }
}

09:14 Now we can make the view controller conform to Loading. The spinner property already exists, so we just have to implement the configure method, where we set the label's text to the episode's title:

final class EpisodeDetailViewController: UIViewController, Loading {
    // ...
    func configure(value: Episode) {
        titleLabel.text = value.title
    }
    // ...
}

09:45 We can now call the protocol's implementation of load in the initializer:

final class EpisodeDetailViewController: UIViewController, Loading {
    convenience init(resource: Resource<Episode>) {
        self.init()
        load(resource)
    }
    // ...
}

10:05 We could also factor out the code that sets up the spinner in viewDidLoad, but we'll keep it as it is for now.

10:12 This is already much better than what we had before, since we've removed a lot of code from the view controller. 10:24 However, it doesn't feel like the perfect solution. 10:34 One drawback is that we're just hiding code that was previously in the view controller in the protocol. The view controller still has a hard dependency on this code though. 10:45 For example, we cannot instantiate an EpisodeDetailViewController without a network stack. This makes testing unnecessarily complex.

Using Container View Controllers

11:06 Let's try a different approach and use container view controllers to separate the part that shows the loading activity from the part that displays the data. The container view controller will make the network call, and once the data comes back, we can add the final view controller as a child. 11:22 We got this idea from a talk by Ayaka Nonaka at try! Swift.

11:33 The first step is to create a new UIViewController called LoadingViewController. 11:46 The initializer takes a resource of anything, so we have to add a generic parameter to the initializer itself. 11:58 As a second parameter, the initializer takes a build function, which takes the result from the network call and returns a view controller:

final class LoadingViewController: UIViewController {
    init<A>(resource: Resource<A>, build: (A) -> UIViewController) {
        // TODO
    }
}

12:12 Now we move the code from the Loading protocol's load method into the initializer. 12:22 To make this work, we also have to move the spinner property from the EpisodeDetailViewController into the LoadingViewController. 12:42 Lastly, instead of calling configure when we have the data from the network, we now call the build function and add the resulting view controller as a child view controller:

init<A>(resource: Resource<A>, build: (A) -> UIViewController) {
    super.init(nibName: nil, bundle: nil)
    spinner.startAnimating()
    sharedWebservice.load(resource) { [weak self] result in
        self?.spinner.stopAnimating()
        guard let value = result.value else { return } // TODO loading error
        let viewController = build(value)
        self?.add(content: viewController)
    }
}

13:10 The add method goes through the standard steps to add a child view controller. 13:24 First, we call addChildViewController and add the child view controller's view as a subview. 13:47 Then, we lay it out by constraining its edges to the container view's edges. 14:06 Finally, we call didMoveToParentViewController on the child view controller:

func add(content content: UIViewController) {
    addChildViewController(content)
    view.addSubview(content.view)
    content.view.translatesAutoresizingMaskIntoConstraints = false
    content.view.constrainEdges(toMarginOf: view)
    content.didMoveToParentViewController(self)
}

14:16 To make the LoadingViewController compile, we also have to add the required initializer. Xcode can fix this for us using the "Fix all in scope" shortcut.

14:24 To add the spinner to the view hierarchy, we copy the code from the EpisodeDetailViewControllers's viewDidLoad method:

override func viewDidLoad() {
    super.viewDidLoad()
    view.backgroundColor = .whiteColor()
    spinner.hidesWhenStopped = true
    spinner.translatesAutoresizingMaskIntoConstraints = false
    view.addSubview(spinner)
    spinner.center(inView: view)
}

14:58 With the LoadingViewController in place, we can now clean up the EpisodeDetailViewController and remove all the unused code. Actually, we can just revert it to its original state.

15:21 To try out the new LoadingViewController, we instantiate it with the episode resource. Then we just return an instance of EpisodeDetailViewController from the build function:

let episodesVC = LoadingViewController(resource: episodeResource, build: { episode in
    return EpisodeDetailViewController(episode: episode)
})

16:38 Another improvement is to directly use EpisodeDetailViewController.init as the build function. In doing this, we don't need the anonymous function around it anymore:

let episodesVC = LoadingViewController(resource: episodeResource, build: EpisodeDetailViewController.init)

17:05 What we like about this approach is that the EpisodeDetailViewController is very simple again. 17:12 It's also synchronous, and therefore pretty easy to test in isolation.

17:24 However, we could still improve the LoadingViewController by removing its dependency on the shared web service. 17:32 Instead of passing in a resource, we could pass in a load function that performs the actual loading of the data. The loading view controller calls the load function with a callback as its parameter. Once this callback gets called, we can proceed as before and call the build function. 17:57 The type of the load parameter is a function with one parameter and no return type. The parameter is a function that takes a Result of A. 18:13 Instead of the call to sharedWebservice.load, we can now call the load function, and everything else stays the same:

init<A>(load: ((Result<A>) -> ()) -> (), build: (A) -> UIViewController) {
    super.init(nibName: nil, bundle: nil)
    spinner.startAnimating()
    load() { [weak self] result in
        self?.spinner.stopAnimating()
        guard let value = result.value else { return } // TODO loading error
        let viewController = build(value)
        self?.add(content: viewController)
    }
}

18:26 Now we move the call to the shared web service down to where we instantiate the LoadingViewController. 18:29 Implementing the load function is straightforward: we get a callback in, and then we call the shared web service with the episode resource and the callback as completion handler:

let sharedWebservice = Webservice()

let episodesVC = LoadingViewController(load: { callback in
    sharedWebservice.load(episodeResource, completion: callback)
}, build: EpisodeDetailViewController.init)

18:52 If we mostly want to create instances of LoadingViewController that load data using a Resource, we could add a convenience initializer in an extension on LoadingViewController in order to avoid code repetition. 19:02 However, it's much better that the loading view controller itself doesn't depend on the shared web service anymore. 19:08 The loading view controller doesn't depend on anything, and the EpisodeDetailViewController is completely decoupled as well, which makes it very easy to test.

Pros and Cons

19:24 There are advantages and disadvantages to both approaches of using a protocol and a container view controller. 19:32 The solution using container view controllers has the problem of nested view controllers that sometimes don't play nicely with UIKit. For example, if you put the LoadingViewController in a navigation stack, it might interfere with UIKit's layout adjustments for the extended edges. 19:52 Despite these issues, it's still very helpful during development. Additionally, sometimes we use it just for a temporary solution. In these cases, it's nice to be able to make any view controller asynchronous almost instantaneously. 20:09 In places where we use child view controllers anyway, this solution could work very nicely in the final code as well.

20:17 Another problem with child view controllers is that navigation items don't really work anymore. 20:25 As such, you'd have to write extra code to make them work. Nevertheless, the LoadingViewController comes in handy in cases where you don't have access to the code of view controllers. For example, we used it to wrap an AVPlayerViewController.

20:48 In the cases where child view controllers don't work very well, you could still factor out the common pattern of loading data asynchronously using a different approach, e.g. the one we showed before using a protocol. 21:03 The protocol solution also has the advantage of being a bit more lightweight, because it doesn't add the extra layer in the view hierarchy.

21:13 Another example where we used the LoadingViewController in our project was as a temporary solution for a table view controller, before we implemented pull-to-refresh. 21:30 Once we had pull-to-refresh, we simply removed this wrapper. It's a handy tool to have in your project for development.

Resources

  • Playground

    Written in Swift 2.2

  • Episode Video

    Become a subscriber to download episode videos.

In Collection

See All Collections

Episode Details

Recent Episodes

See All

Unlock Full Access

Subscribe to Swift Talk

  • Watch All Episodes

    A new episode every week

  • icon-benefit-download Created with Sketch.

    Download Episodes

    Take Swift Talk with you when you're offline

  • Support Us

    With your help we can keep producing new episodes