In this episode, we create a view model for the day view controller. Fire up Xcode and open the starter project of this episode. We start by creating a new group, View Models, in the Weather View Controllers group. I prefer to keep the view models close to the view controllers in which they are used.
Create a new Swift file in the View Models group and name it DayViewModel.swift.
DayViewModel
is a struct, a value type. Remember that the view model should keep a reference to the model, which means we need to create a property for it. That is all we need to do to create our first view model.
DayViewModel.swift
import Foundation
struct DayViewModel {
// MARK: - Properties
let weatherData: WeatherData
}
Creating the Public Interface
The next step is moving the code located in the updateWeatherDataContainerView(with:)
method of the DayViewController
class to the view model. What we need to focus on are the values we use to populate the user interface.
Date Label
Let's start with the date label. The date label expects a formatted date and it needs to be of type String
. It is the responsibility of the view model to ask the model for the value of its time
property and transform that value to the format the date label expects.
Let's start by creating a computed property in the view model. We name it date
and it should be of type String
.
DayViewModel.swift
var date: String {
}
We initialize a DateFormatter
instance to convert the date to a formatted string and set the date formatter's dateFormat
property. We invoke the date formatter's string(from:)
method and return the result. That is it for the date label.
DayViewModel.swift
var date: String {
// Initialize Date Formatter
let dateFormatter = DateFormatter()
// Configure Date Formatter
dateFormatter.dateFormat = "EEE, MMMM d"
return dateFormatter.string(from: weatherData.time)
}
Time Label
We can repeat this for the time label. We create a time
computed property of type String
. The implementation is similar. We create a DateFormatter
instance, set its dateFormat
property, and return a formatted string.
DayViewModel.swift
var time: String {
// Initialize Date Formatter
let dateFormatter = DateFormatter()
// Configure Date Formatter
dateFormatter.dateFormat = ""
return dateFormatter.string(from: weatherData.time)
}
There is one complication, though. The format of the time depends on the user's preferences. That is easy to solve, though. Navigate to TimeNotation.swift in the Types group. We add a computed property, dateFormat
, to the TimeNotation
enum. The dateFormat
computed property returns the correct date format based on the user's preferences.
UserDefaults.swift
enum TimeNotation: Int {
// MARK: - Cases
case twelveHour
case twentyFourHour
// MARK: - Properties
var dateFormat: String {
switch self {
case .twelveHour: return "hh:mm a"
case .twentyFourHour: return "HH:mm"
}
}
}
We can now update the implementation of the time
computed property in DayViewModel.swift.
DayViewModel.swift
var time: String {
// Initialize Date Formatter
let dateFormatter = DateFormatter()
// Configure Date Formatter
dateFormatter.dateFormat = UserDefaults.timeNotation.dateFormat
return dateFormatter.string(from: weatherData.time)
}
Let me explain what is happening. timeNotation
is a class computed property of the UserDefaults
class. You can find its implementation in UserDefaults.swift in the Extensions group. It returns a TimeNotation
object.
UserDefaults.swift
// MARK: - Time Notation
class var timeNotation: TimeNotation {
get {
let storedValue = UserDefaults.standard.integer(forKey: Keys.timeNotation)
return TimeNotation(rawValue: storedValue) ?? TimeNotation.twelveHour
}
set {
UserDefaults.standard.set(newValue.rawValue, forKey: Keys.timeNotation)
}
}
We load the user's preference from the user defaults database and use the value to create a TimeNotation
object. We use the same technique for the user's other preferences.
Description Label
Populating the description label is easy. We define a computed property in the view model, summary
, of type String
and return the value of the summary
property of the model.
DayViewModel.swift
var summary: String {
weatherData.summary
}
Temperature Label
The value for the temperature label is a bit more complicated because we need to take the user's preferences into account. We start simple. We create another computed property in which we store the temperature in a constant, temperature
.
DayViewModel.swift
var temperature: String {
let temperature = weatherData.temperature
}
We fetch the user's preference and format the value stored in the temperature
constant based on the user's preference. We need to convert the temperature if the user's preference is set to degrees Celcius.
DayViewModel.swift
var temperature: String {
let temperature = weatherData.temperature
switch UserDefaults.temperatureNotation {
case .fahrenheit:
return String(format: "%.1f °F", temperature)
case .celsius:
return String(format: "%.1f °C", temperature.toCelcius)
}
}
The implementation of the temperatureNotation
class computed property is very similar to the timeNotation
class computed property we looked at earlier.
UserDefaults.swift
// MARK: - Temperature Notation
class var temperatureNotation: TemperatureNotation {
get {
let storedValue = UserDefaults.standard.integer(forKey: Keys.temperatureNotation)
return TemperatureNotation(rawValue: storedValue) ?? TemperatureNotation.fahrenheit
}
set {
UserDefaults.standard.set(newValue.rawValue, forKey: Keys.temperatureNotation)
}
}
Wind Speed Label
Populating the wind speed label is very similar. Because the wind speed label expects a string, we create a windSpeed
computed property of type String
. We ask the model for the the value of its windSpeed
property and format that value based on the user's preference.
DayViewModel.swift
var windSpeed: String {
let windSpeed = weatherData.windSpeed
switch UserDefaults.unitsNotation {
case .imperial:
return String(format: "%.f MPH", windSpeed)
case .metric:
return String(format: "%.f KPH", windSpeed.toKPH)
}
}
The implementation of the unitsNotation
class computed property is very similar to the timeNotation
and temperatureNotation
class computed properties we looked at earlier.
UserDefaults.swift
// MARK: - Units Notation
class var unitsNotation: UnitsNotation {
get {
let storedValue = UserDefaults.standard.integer(forKey: Keys.unitsNotation)
return UnitsNotation(rawValue: storedValue) ?? UnitsNotation.imperial
}
set {
UserDefaults.standard.set(newValue.rawValue, forKey: Keys.unitsNotation)
}
}
Icon Image View
For the icon image view, we need an image. We could put this logic in the view model. However, because we need the same logic later, in the view model of the week view controller, it is better to create an extension for UIImage
in which we put that logic.
Create a new file in the Extensions group and name it UIImage.swift. Create an extension for the UIImage
class and define a class method imageForIcon(with:)
.
UIImage.swift
import UIKit
extension UIImage {
class func imageForIcon(with name: String) -> UIImage? {
}
}
We simplify the current implementation of the weather view controller. We use the value of the name
argument to instantiate the UIImage
instance in most cases of the switch
statement. I really like how flexible the switch
statement is in Swift. Notice that we also return a UIImage
instance in the default
case of the switch
statement.
UIImage.swift
import UIKit
extension UIImage {
class func imageForIcon(with name: String) -> UIImage? {
switch name {
case "clear-day", "clear-night", "rain", "snow", "sleet": return UIImage(named: name)
case "wind", "cloudy", "partly-cloudy-day", "partly-cloudy-night": return UIImage(named: "cloudy")
default: return UIImage(named: "clear-day")
}
}
}
With this method in place, it is easy to populate the icon image view. We create a computed property of type UIImage?
in the view model and name it image
. In the body of the computed property, we invoke the class method we just created, passing in the value of the model's icon
property.
DayViewModel.swift
var image: UIImage? {
UIImage.imageForIcon(with: weatherData.icon)
}
Because UIImage
is defined in the UIKit framework, we need to replace the import statement for Foundation with an import statement for UIKit.
DayViewModel.swift
import UIKit
struct DayViewModel {
...
}
This is a code smell. Whenever you import UIKit in a view model, a warning bell should go off. The view model shouldn't need to know anything about views or the user interface. In this example, however, we have no other option. Since we want to return a UIImage
instance, we need to import UIKit. If you don't like this, you can also return the name of the image and have the view controller be in charge of creating the UIImage
instance. That is up to you.
I want to make two small improvements. The DateFormatter
instances shouldn't be created in the computed properties. Every time the date
and time
computed properties are accessed, a DateFormatter
instance is created. We can make the implementation of the DayViewModel
struct more efficient by creating a property with name dateFormatter
. We create and assign a DateFormatter
instance to the dateFormatter
property.
DayViewModel.swift
import UIKit
struct DayViewModel {
// MARK: - Properties
let weatherData: WeatherData
// MARK: -
private let dateFormatter = DateFormatter()
// MARK: - Public API
var date: String {
// Configure Date Formatter
dateFormatter.dateFormat = "EEE, MMMM d"
return dateFormatter.string(from: weatherData.time)
}
var time: String {
// Configure Date Formatter
dateFormatter.dateFormat = UserDefaults.timeNotation.dateFormat
return dateFormatter.string(from: weatherData.time)
}
...
}
In the date
and time
computed properties, the DateFormatter
instance is configured by setting its dateFormat
property. This implementation is more efficient. It is a small improvement but nonetheless an improvement.
What's Next?
You have created your very first view model. In the next episode, we put it to use in the day view controller.