Now that you have an idea of the ins and outs of the project, I would like to take a few minutes to highlight some of the problems the project suffers from. Because we are working with a small project, the problems we resolve in this series are subtle. That is why we take a few minutes to explore them in this episode.
Day View Controller
We start with the day view controller. The first thing to point out is that the 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 pattern, a controller referencing a model is a red flag when we adopt the Model-View-ViewModel pattern.
DayViewController.swift
var now: WeatherData? {
didSet {
updateView()
}
}
The second and most important problem is the implementation of the updateWeatherDataContainerView(with:)
method. This is another example that is typical for the Model-View-Controller pattern. The raw values of the model object are transformed and formatted before they are displayed to the user.
DayViewController.swift
private func updateWeatherDataContainerView(with weatherData: WeatherData) {
weatherDataContainerView.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 controller be in charge of this task? Maybe. Maybe not. But there is a more elegant solution.
If we adopt the Model-View-ViewModel pattern, the controller will no longer be responsible for data manipulation. What's more, the controller won't know about and have direct access to the model object. It will receive a view model from the root view controller and use that view model to populate its view. That is the task it was designed for, controlling a view.
Week View Controller
The week view controller suffers from the same problems. It keeps a reference to the array of WeatherDayData
objects and uses it to populate its table view.
WeekViewController.swift
var week: [WeatherDayData]? {
didSet {
updateView()
}
}
In the tableView(_:cellForRowAt:)
method, the controller accesses the WeatherDayData
object that corresponds with the index path and uses it to populate a table view cell. The raw values of the model object are transformed and formatted before they are 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("Unable to Dequeue Weather Day 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
}
The tableView(_:cellForRowAt:)
method is sprinkled with a handful of if
statements. The if
statements ensure that the raw values of the model object 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 raw values of the model object. If we use the Model-View-ViewModel pattern, we can clean this up too. Whenever I see a DateFormatter
property in a controller, I know it is time for some refactoring.
Locations View Controller
Later in this series, we focus on the locations view controller. I will show you how user interaction is handled by the Model-View-ViewModel pattern. That is 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 elegant, readable, and maintainable.
Settings View Controller
There doesn't seem to be anything wrong with the settings view controller. It is true that it doesn't look too bad, but I assure you that it will look a lot better after we have 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.