I'd like to give the user the ability to manually refresh the weather data. While this isn't strictly necessary since the application refreshes the weather data every time the application is opened, it gives me the opportunity to show you how to implement pull to refresh, a common design pattern in mobile applications.

Adding a Refresh Control

The user should be able to pull down the table view of the week view controller to refresh the weather data the application displays. Adding pull to refresh is easy to implement thanks to the UIRefreshControl class. Open WeekViewController.swift and define a private lazy variable property, refreshControl, of type UIRefreshControl. We instantiate the UIRefreshControl instance in a closure. Don't forget to append a pair of parentheses to the closure.

private lazy var refreshControl: UIRefreshControl = {

}()

In the closure, we initialize a UIRefreshControl instance. As the name implies, the UIRefreshControl class is a UIControl subclass. We need to define what happens when the user pulls down the table view. We invoke the addTarget(_:action:for:) method to associate a target object and action method with the control for a particular event. The target is the WeekViewController instance, the action is a method we implement later, refresh(_:), and the event is valueChanged.

private lazy var refreshControl: UIRefreshControl = {
    // Initialize Refresh Control
    let refreshControl = UIRefreshControl()

    // Configure Refresh Control
    refreshControl.addTarget(self, action: #selector(refresh(_:)), for: .valueChanged)

    return refreshControl
}()

Every time the user pulls the refresh control, the refresh(_:) method is invoked. We can also customize the appearance of the refresh control by setting its tint color to the base tint color we defined earlier in this series.

private lazy var refreshControl: UIRefreshControl = {
    // Initialize Refresh Control
    let refreshControl = UIRefreshControl()

    // Configure Refresh Control
    refreshControl.tintColor = UIColor.Rainstorm.baseTintColor
    refreshControl.addTarget(self, action: #selector(refresh(_:)), for: .valueChanged)

    return refreshControl
}()

With the refresh control ready to use, we need to integrate it into the table view. That's very easy to do. In the didSet property observer of the tableView property, we assign the refresh control to the refreshControl property of the table view. The table view takes care of positioning and sizing the refresh control. That isn't something we need to worry about.

@IBOutlet var tableView: UITableView! {
    didSet {
        tableView.isHidden = true
        tableView.dataSource = self
        tableView.separatorInset = .zero
        tableView.estimatedRowHeight = 44.0
        tableView.showsVerticalScrollIndicator = false
        tableView.rowHeight = UITableViewAutomaticDimension

        // Set Refresh Control
        tableView.refreshControl = refreshControl
    }
}

Before we decide what happens when the user pulls the refresh control, we add a stub for the refresh(_:) method.

// MARK: - Actions

private func refresh(_ sender: UIRefreshControl) {

}

Even though we added a stub for the refresh(_:) method, the compiler isn't satisfied yet. It throws an error. Let's take a closer look at the error.

The compiler isn't satisfied yet.

The UIRefreshControl class takes advantage of the target-action pattern. The target-action pattern relies on the Objective-C runtime and that means the refresh(_:) method needs to be exposed to Objective-C. That is why the compiler suggests to prefix the refresh(_:) method with the objc attribute.

// MARK: - Actions

@objc private func refresh(_ sender: UIRefreshControl) {

}

What should the week view controller do if the user pulls the refresh control? The RootViewModel class is responsible for fetching the location and the weather data. We need to find a way to notify the RootViewModel instance. We could use notifications, but that isn't an approach I recommend for this type of communication. If one object needs to communicate with another object, closures or delegation are better options. Using a closure or a handler is the simplest solution, but let me show you how to use the delegate pattern to solve the problem.

We first define a delegate protocol at the top of WeekViewController.swift. We define a protocol with name WeekViewControllerDelegate. The protocol defines one method, controllerDidRefresh(_:). It's a best practice to always pass the delegating object, the week view controller, as the first argument.

protocol WeekViewControllerDelegate {
    func controllerDidRefresh(_ controller: WeekViewController)
}

The week view controller delegates work to another object. That means it needs to hold a reference to the object it delegates work to. We define a property delegate of type WeekViewControllerDelegate?. We prefix the property declaration with the weak keyword to make sure the week view controller doesn't hold a strong reference to its delegate. That is important to prevent a retain cycle.

final class WeekViewController: UIViewController {

    // MARK: - Properties

    weak var delegate: WeekViewControllerDelegate?

    ...

}

But there's a problem. The compiler informs us that the weak keyword only makes sense to reference types. A struct, for example, can't be weakly referenced. It's a value type, not a reference type.

The compiler informs us that the weak keyword only makes sense to reference types.

The solution is simple. We need to make it clear that the WeekViewControllerDelegate protocol can only be adopted by classes. We add a requirement to the protocol definition that enforces this requirement.

protocol WeekViewControllerDelegate: class {
    func controllerDidRefresh(_ controller: WeekViewController)
}

Before we put the WeekViewControllerDelegate protocol to use, we need to implement the refresh(_:) method. The implementation is simple. We notify the delegate by invoking the controllerDidRefresh(_:) method. That's it.

// MARK: - Actions

@objc private func refresh(_ sender: UIRefreshControl) {
    // Notify Delegate
    delegate?.controllerDidRefresh(self)
}

Conforming to the Protocol

It's the root view controller that acts as the delegate of the week view controller. Open RootViewController.swift and navigate to the definition of the weekViewController property. We set the RootViewController instance as the delegate of the WeekViewController instance.

private let weekViewController: WeekViewController = {
    guard let weekViewController = UIStoryboard.main.instantiateViewController(withIdentifier: WeekViewController.storyboardIdentifier) as? WeekViewController else {
        fatalError("Unable to Instantiate Week View Controller")
    }

    // Configure Week View Controller
    weekViewController.delegate = self
    weekViewController.view.translatesAutoresizingMaskIntoConstraints = false

    return weekViewController
}()

But there's a problem. We try to reference the RootViewController instance in the closure in which the WeekViewController instance is created.

We try to reference self in the closure in which the WeekViewController instance is created.

The solution is simple. If we define the weekViewController property as a lazy variable property, we can reference the RootViewController instance in the closure. That's a trick we used earlier in this series.

private lazy var weekViewController: WeekViewController = {
    guard let weekViewController = UIStoryboard.main.instantiateViewController(withIdentifier: WeekViewController.storyboardIdentifier) as? WeekViewController else {
        fatalError("Unable to Instantiate Week View Controller")
    }

    // Configure Week View Controller
    weekViewController.delegate = self
    weekViewController.view.translatesAutoresizingMaskIntoConstraints = false

    return weekViewController
}()

The compiler still complains. We need to conform the RootViewController class to the WeekViewControllerDelegate protocol. At the bottom of RootViewController.swift, we create an extension for the RootViewController class and use the extension to conform RootViewController to the WeekViewControllerDelegate protocol. We add a stub for the only method of the WeekViewControllerDelegate protocol.

extension RootViewController: WeekViewControllerDelegate {

    func controllerDidRefresh(_ controller: WeekViewController) {

    }

}

The implementation is surprisingly simple. We invoke the refresh() method of the RootViewModel class we implemented in the previous episode.

extension RootViewController: WeekViewControllerDelegate {

    func controllerDidRefresh(_ controller: WeekViewController) {
        viewModel?.refresh()
    }

}

This won't work as long as the refresh() method is declared privately. Open RootViewModel.swift, navigate to the refresh() method, and remove the private keyword.

func refresh() {
    fetchLocation()
}

Build and run the application to test the implementation. Open RootViewController.swift and navigate to the setupViewModel(with:) method. Set a breakpoint in the closure we assigned to the didFetchWeatherData property of the view model. The closure should be invoked every time the user pulls down the table view of the week view controller.

Pull down the table view to see what happens. The breakpoint is hit as expected. Click the Continue button in the debug bar at the bottom to resume the execution of the application. There seems to be a problem. The refresh control doesn't disappear.

Hiding the Refresh Control

A refresh control is a dumb view. It's not able to figure out when it's time to disappear. We need to invoke endRefreshing() on the refresh control to inform it that the refresh operation has ended. The question is "How do we do that?"

The refresh control is managed by the week view controller. We could implement a method in the WeekViewController class for that purpose, but that doesn't feel right. The root view controller is the parent of the week view controller and it shouldn't know about the implementation of the week view controller.

Posting a notification is another option, but that too isn't a solution that I like. I tend to use notifications sparingly and this isn't a scenario that asks for notifications.

As the example illustrates, view controller containment can make simple problems more complex. There's one solution I find acceptable. We invoke endRefreshing() on the refresh control every time the viewModel property of the week view controller is set.

Open WeekViewController.swift and navigate to the viewModel property. We invoke endRefreshing() on the refresh control before the guard statement. The guard statement makes less sense with the updated implementation. Let's replace it with an if statement.

var viewModel: WeekViewModel? {
    didSet {
        // Hide Refresh Control
        refreshControl.endRefreshing()

        if let viewModel = viewModel {
            // Setup View Model
            setupViewModel(with: viewModel)
        }
    }
}

That's the only change we need to make. But there's a caveat. What happens if the application isn't able to obtain the location of the device or if the request to the Dark Sky API fails? Revisit RootViewController.swift and navigate to the setupViewModel(with:) method. If a problem occurs, we need to set the view models of the child view controllers to nil. That's the only implementation detail I'm not quite happy with, but it's important that we hide the refresh control if something goes wrong.

private func setupViewModel(with viewModel: RootViewModel) {
    // Configure View Model
    viewModel.didFetchWeatherData = { [weak self] (result) in
        switch result {
        case .success(let weatherData):
            ...
        case .failure(let error):
            let alertType: AlertType

            switch error {
            case .notAuthorizedToRequestLocation:
                alertType = .notAuthorizedToRequestLocation
            case .failedToRequestLocation:
                alertType = .failedToRequestLocation
            case .noWeatherDataAvailable:
                alertType = .noWeatherDataAvailable
            }

            // Notify User
            self?.presentAlert(of: alertType)

            // Update Child View Controllers
            self?.dayViewController.viewModel = nil
            self?.weekViewController.viewModel = nil
        }
    }
}

Build and run the application one more time to see if the refresh control is hidden when the refresh operation has ended.

What's Next?

Adding pull to refresh to an application used to be tedious. This is no longer true thanks to the UIRefreshControl class.

I hope this episode has also illustrated that view controller containment has its downsides. By breaking a user interface up into smaller components, each managed by a view controller, it can be tricky to elegantly communicate between these components. But that's a downside I'm happy to accept. View controller containment has helped us keep the implementation of Rainstorm simple and modular. Each of the child view controllers is responsible for one aspect of the user interface.