Mastering MVVM With Swift

What Is Wrong With Cloudy

Resources

Now that you have an idea of the ins and outs of Cloudy, I'd like to take a few minutes to highlight some of Cloudy's issues. Keep in mind that Cloudy is a small project. The problems we're going to fix with the Model-View-ViewModel pattern are less apparent, which is why I'd like to highlight them before we fix them.

Day View Controller

We start with the day view controller. The first thing to point out is that the view controller keeps a reference to the model. This is a classic example of the Model-View-Controller pattern. Even though there isn't anything inherently wrong with this, when we adopt the Model-View-ViewModel pattern, this will change.

DayViewController.swift

var now: WeatherData? {
    didSet {
        updateView()
    }
}

The second and most important problem is the implementation of the updateWeatherDataContainer(withWeatherData:) method. This is another pattern that's typical for the Model-View-Controller pattern. The raw values of the model data are transformed and formatted before they're displayed to the user.

DayViewController.swift

private func updateWeatherDataContainer(withWeatherData weatherData: WeatherData) {
    weatherDataContainer.isHidden = false

    var windSpeed = weatherData.windSpeed
    var temperature = weatherData.temperature

    let dateFormatter = DateFormatter()
    dateFormatter.dateFormat = "EEE, MMMM d"
    dateLabel.text = dateFormatter.string(from: weatherData.time)

    let timeFormatter = DateFormatter()

    if UserDefaults.timeNotation() == .twelveHour {
        timeFormatter.dateFormat = "hh:mm a"
    } else {
        timeFormatter.dateFormat = "HH:mm"
    }

    timeLabel.text = timeFormatter.string(from: weatherData.time)

    descriptionLabel.text = weatherData.summary

    if UserDefaults.temperatureNotation() != .fahrenheit {
        temperature = temperature.toCelcius()
        temperatureLabel.text = String(format: "%.1f °C", temperature)
    } else {
        temperatureLabel.text = String(format: "%.1f °F", temperature)
    }

    if UserDefaults.unitsNotation() != .imperial {
        windSpeed = windSpeed.toKPH()
        windSpeedLabel.text = String(format: "%.f KPH", windSpeed)
    } else {
        windSpeedLabel.text = String(format: "%.f MPH", windSpeed)
    }

    iconImageView.image = imageForIcon(withName: weatherData.icon)
}

Should the view controller be in charge of this task? Maybe. Maybe not. But is there a more elegant solution? Absolutely.

If we adopt the Model-View-ViewModel pattern, the view controller will no longer be responsible for data manipulation. Moreover, the view controller won't know about and have direct access to the model. It will receive a view model from the root view controller and use the view model to populate its view. That's the task it was designed for, controlling a view.

Week View Controller

The week view controller suffers from the same problems. It keeps a strong reference to the array of WeatherDayData objects and uses them to populate the table view.

WeekViewController.swift

var week: [WeatherDayData]? {
    didSet {
        updateView()
    }
}

In the tableView(_:cellForRowAt:) method, a WeatherDayData instance is fetched from the array and it's used to populate a table view cell. The raw values of the model data are transformed and formatted before they're displayed to the user.

WeekViewController.swift

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    guard let cell = tableView.dequeueReusableCell(withIdentifier: WeatherDayTableViewCell.reuseIdentifier, for: indexPath) as? WeatherDayTableViewCell else { fatalError("Unexpected Table View Cell") }

    if let week = week {
        // Fetch Weather Data
        let weatherData = week[indexPath.row]

        var windSpeed = weatherData.windSpeed
        var temperatureMin = weatherData.temperatureMin
        var temperatureMax = weatherData.temperatureMax

        if UserDefaults.temperatureNotation() != .fahrenheit {
            temperatureMin = temperatureMin.toCelcius()
            temperatureMax = temperatureMax.toCelcius()
        }

        // Configure Cell
        cell.dayLabel.text = dayFormatter.string(from: weatherData.time)
        cell.dateLabel.text = dateFormatter.string(from: weatherData.time)

        let min = String(format: "%.0f°", temperatureMin)
        let max = String(format: "%.0f°", temperatureMax)

        cell.temperatureLabel.text = "\(min) - \(max)"

        if UserDefaults.unitsNotation() != .imperial {
            windSpeed = windSpeed.toKPH()
            cell.windSpeedLabel.text = String(format: "%.f KPH", windSpeed)
        } else {
            cell.windSpeedLabel.text = String(format: "%.f MPH", windSpeed)
        }

        cell.iconImageView.image = imageForIcon(withName: weatherData.icon)
    }

    return cell
}

We also see several if statements to make sure the raw values are formatted correctly, based on the user’s preferences.

The Model-View-Controller pattern has a few other consequences. The week view controller has a couple of properties of type DateFormatter to format the model data that's displayed in the table view. If we use the Model-View-ViewModel pattern, we can clean this up too. Whenever I see a DateFormatter property in a view controller, I know it’s time for some refactoring.

Locations View Controller

Later in this series, we focus on the locations view controller. I'll show you how user interaction is handled by the Model-View-ViewModel pattern. That's a bit more complicated. However, once you understand the ins and outs of the Model-View-ViewModel pattern, this won't be difficult to understand. I promise you that the result is pure elegance.

Settings View Controller

There doesn’t seem to be anything wrong with the settings view controller. It's true that it doesn’t look too bad, but I assure you that it'll look a lot better after we've given the settings view controller a facelift using protocols and MVVM.

What's Next

In the next episodes, you create your very first view model. We start with the view model for the day view controller.

Resources
Next Episode "A Quick Recap"

Stop Writing Swift That Sucks

Download the Swift Patterns I Swear By