In the first installment of this series, we created the project in Xcode, updated the project's structure, and put it under source control. This lesson focuses on building the basic user interface of the application.

If you want to follow along, open the project you created in the previous installment or clone the GitHub repository.

Building the User Interface

The weather application we're building runs on iPhone and iPad. We want to create a user interface that looks great and feels right on both device families. There are several ways to accomplish this. The approach I have chosen for takes advantage of size classes.

Thunderstorm will present three views to the user:

  • the current weather
  • the forecast for the day
  • the forecast for the week

On devices with a compact, horizontal size class (for example, iPhone SE), the user can navigate between these views by swiping left and right. On devices with a regular, compact size class (for example, iPad Pro), these views are presented side by side. To accomplish this, we make use of a collection view.

Compact Horizontal Size Class

Regular Horizontal Size Class

Creating the RootViewController Class

The view controller that manages the collection view will be the root view controller of the project. Select the ViewController class in the Project Navigator and delete it from the project. Create a new group in the View Controllers group, name it Root View Controller, and link it to a folder named Root View Controller. We covered the relation between groups and folders in the previous lesson.

Add a new UIViewController class to the Root View Controller group and name it RootViewController. There is no need to create a XIB file for the class as we will be using storyboards.

Create Root View Controller Class

Create Root View Controller Class

Create an outlet for the collection view and make the RootViewController class conform to the UICollectionViewDataSource and UICollectionViewDelegateFlowLayout protocols. Notice that I created an extension for each protocol. This is a nice trick to keep related code organized.

import UIKit

class RootViewController: UIViewController {

    // MARK: - Properties

    @IBOutlet var collectionView: UICollectionView!

    // MARK: - View Life Cycle

    override func viewDidLoad() {
        super.viewDidLoad()
    }

}

extension RootViewController: UICollectionViewDataSource {

}

extension RootViewController: UICollectionViewDelegateFlowLayout {

}

Open Main.storyboard and the select the view controller of the View Controller Scene. Open the Identity Inspector on the right and set Class to RootViewController.

Create Root View Controller Class

Drag a collection view from the Object Library to the view of the root view controller and make it span the width and height of the view. Connect the collection view to the collectionView outlet of the RootViewController instance.

Create Root View Controller Class

Add the necessary layout constraints to pin the collection view to the top, bottom, leading, and trailing edges of the view of the root view controller.

Create Root View Controller Class

Select the collection view, open the Connections Inspector on the right, and set its dataSource and delegate properties to the view controller of the scene, that is, the root view controller.

Create Root View Controller Class

With the collection view selected, open the Attributes Inspector and:

  • set Items to 0
  • set Scroll Direction to Horizontal
  • uncheck Shows Horizontal Indicator
  • uncheck Shows Vertical Indicator
  • check Paging Enabled

Create Root View Controller Class

If you have some experience building iOS applications, then these steps should not pose any problems.

Populating the Collection View

Before we can build the application, we first need to implement the required methods of the UICollectionViewDataSource protocol. Open the RootViewController class and define an enum, RootViewType, to help us with this. Note that we declare the enum within the RootViewController class.

import UIKit

class RootViewController: UIViewController {

    enum RootViewType: Int {
        case now = 0
        case day
        case week

        static var count: Int {
            return RootViewType.week.rawValue + 1
        }
    }

    // MARK: - Properties

    @IBOutlet var collectionView: UICollectionView!

    // MARK: - View Life Cycle

    override func viewDidLoad() {
        super.viewDidLoad()
    }

}

This makes working with the collection view easier and more elegant. The static computed property count helps us implement the collectionView(_:numberOfItemsInSection:) method of the UICollectionViewDataSource protocol.

extension RootViewController: UICollectionViewDataSource {

    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return RootViewType.count
    }

}

We also need to tell the collection view which collection view cells it needs to display. In the viewDidLoad() method, we invoke a helper method, setupView().

// MARK: - View Life Cycle

override func viewDidLoad() {
    super.viewDidLoad()

    setupView()
}

In setupView(), we invoke another helper method, setupCollectionView(). I like to keep methods short and concise.

// MARK: - View Methods

private func setupView() {
    setupCollectionView()
}

In setupCollectionView(), we register the UICollectionViewCell class for the someCell reuse identifier. We clean up the implementation of setupCollectionView() later in this lesson.

// MARK: -

private func setupCollectionView() {
    collectionView.register(UICollectionViewCell.classForCoder(), forCellWithReuseIdentifier: "someCell")
}

We can now implement the collectionView(_:cellForItemAt:) method of the UICollectionViewDataSource protocol. We set the background color of the content view of the collection view cell to a green color to visualize what we have so far.

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    // Dequeue Reusable Cell
    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "someCell", for: indexPath)

    // Configure Cell
    cell.contentView.backgroundColor = .green

    return cell
}

Build and run the application in the simulator.

Populating the Collection View

Adding More View Controllers

It's time to add the view controllers for each root view type:

  • day
  • now
  • week

Each collection view cell of the collection view contains a view controller. For that, we need to create three UICollectionViewCell subclasses and three UIViewController subclasses.

Open the Project Navigator and create three groups and corresponding folders in the View Controllers group. Name them:

  • Day View Controller
  • Now View Controller
  • Week View Controller

Create three UIViewController subclasses and add each subclass to its corresponding group:

  • DayViewController
  • NowViewController
  • WeekViewController

The implementation of these view controllers is very basic for now.

import UIKit

class DayViewController: UIViewController {

    // MARK: - View Life Cycle

    override func viewDidLoad() {
        super.viewDidLoad()

        setupView()
    }

    // MARK: - View Methods
    private func setupView() {

    }

}
import UIKit

class NowViewController: UIViewController {

    // MARK: - View Life Cycle

    override func viewDidLoad() {
        super.viewDidLoad()

        setupView()
    }

    // MARK: - View Methods
    private func setupView() {

    }

}
import UIKit

class WeekViewController: UIViewController {

    // MARK: - View Life Cycle

    override func viewDidLoad() {
        super.viewDidLoad()

        setupView()
    }

    // MARK: - View Methods
    private func setupView() {

    }

}

Subclassing UICollectionViewCell

Add a group and corresponding folder named Collection View Cells to the Root View Controller group. We need to create a UICollectionViewCell subclass for every root view type.

  • DayCollectionViewCell
  • NowCollectionViewCell
  • WeekCollectionViewCell

The implementation is very similar and easy to understand. We declare a static property for the reuse identifier of the UICollectionViewCell subclass and a constant property, viewController, for the UIViewController subclasses we created earlier.

In the initializers, we instantiate the view controller. We also invoke setupViewController(), a helper method in which we configure the view controller.

import UIKit

class DayCollectionViewCell: UICollectionViewCell {

    // MARK: - Type Properties

    static let reuseIdentifier = "DayCollectionViewCell"

    // MARK: - Properties

    let viewController: DayViewController

    // MARK: - Initialization

    override init(frame: CGRect) {
        // Initialize View Controller
        viewController = DayViewController()

        super.init(frame: frame)

        setupViewController()
    }

    required init?(coder aDecoder: NSCoder) {
        // Initialize View Controller
        viewController = DayViewController()

        super.init(coder: aDecoder)

        setupViewController()
    }

    private func setupViewController() {
        // Configure View Controller
        viewController.view.backgroundColor = .blue

        // Add View Controller to Content View
        contentView.addSubview(viewController.view)

        if let view = viewController.view {
            view.translatesAutoresizingMaskIntoConstraints = false

            view.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 0.0).isActive = true
            view.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: 0.0).isActive = true
            view.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 0.0).isActive = true
            view.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: 0.0).isActive = true
        }
    }

}

The only difference between the UICollectionViewCell subclasses is the reuse identifier, the type of the viewController property, and the background color of the view controller.

It's now time to update the RootViewController class. We start by updating the setupCollectionView() method.

private func setupCollectionView() {
    collectionView.register(NowCollectionViewCell.classForCoder(), forCellWithReuseIdentifier: NowCollectionViewCell.reuseIdentifier)
    collectionView.register(DayCollectionViewCell.classForCoder(), forCellWithReuseIdentifier: DayCollectionViewCell.reuseIdentifier)
    collectionView.register(WeekCollectionViewCell.classForCoder(), forCellWithReuseIdentifier: WeekCollectionViewCell.reuseIdentifier)
}

This allows us to update the collectionView(_:cellForItemAt:) method of the UICollectionViewDataSource protocol.

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    guard let type = RootViewType(rawValue: indexPath.item) else {
        fatalError()
    }

    switch type {
    case .day:
        // Dequeue Reusable Cell
        return collectionView.dequeueReusableCell(withReuseIdentifier: NowCollectionViewCell.reuseIdentifier, for: indexPath)
    case .now:
        // Dequeue Reusable Cell
        return collectionView.dequeueReusableCell(withReuseIdentifier: DayCollectionViewCell.reuseIdentifier, for: indexPath)
    case .week:
        // Dequeue Reusable Cell
        return collectionView.dequeueReusableCell(withReuseIdentifier: WeekCollectionViewCell.reuseIdentifier, for: indexPath)
    }
}

We create an instance of the RootViewType enum and dequeue a collection view cell based on the value of the enum. Notice that we throw a fatal error if we're unable to create a RootViewType instance. You can read more about the why and how here.

If you build and run the application, you should see three collection view cells, each with a different color. This is a sign that we're on the right track.

We are on the right track.

Sizing the Collection View Cells

The last piece of this puzzle is correctly setting the size of the collection view cells. Even though the user interface looks different on iPhone and iPad, it's more correct to say that the user interface looks different depending on the size class.

If the size class for the horizontal dimension is compact, the collection view cell spans the width of the collection view. If the size class for the horizontal dimension is regular, the collection view cell spans a third of the width of the collection view. To make this work, we implement four methods of the UICollectionViewDelegateFlowLayout protocol.

extension RootViewController: UICollectionViewDelegateFlowLayout {

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        let bounds = collectionView.bounds

        return CGSize(width: (bounds.width * aspectRatio), height: bounds.height)
    }

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
        return minimumInteritemSpacingForSection
    }

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
        return minimumLineSpacingForSection
    }

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
        return insetForSection
    }

}

As you can see, I've used several helper properties to clean up the implementation of these methods. I try to avoid hardcoded values in method implementations as much as possible.

// MARK: - Properties

@IBOutlet var collectionView: UICollectionView!

// MARK: -

fileprivate var aspectRatio: CGFloat {
    switch traitCollection.horizontalSizeClass {
    case .compact:
        return 1.0
    default:
        return 1.0 / 3.0
    }
}

fileprivate let minimumInteritemSpacingForSection: CGFloat = 0.0
fileprivate let minimumLineSpacingForSection: CGFloat = 0.0
fileprivate let insetForSection = UIEdgeInsets()

The most interesting property is aspectRatio, a computed property whose value is based on the horizontal size class of the view controller's traitCollection property.

If you build and run the application, you notice that everything looks fine ... sort of. There's one issue we need to address. We need to update the layout of the collection view when the size of the view controller's view changes. We can do this by overriding a method of the UIContentContainer protocol, a protocol every UIViewController conforms to. The method we need to override is viewWillTransition(to:with:).

// MARK: - Content Container Methods

override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
    collectionView.collectionViewLayout.invalidateLayout()
}

Whenever the size of the view changes, we invalidate the layout of the collection view layout. This ensures the collection view is updated correctly. Build and run the application again. Rotate the device a few times to make sure everything is working as expected.

What's Next?

With a basic user interface in place, it's time to start focusing on populating the application with data. We do that in the next installment.