To adopt the Model-View-ViewModel pattern in the WeekViewController
class, we first need to create a new view model. Create a new file in the View Models group and name the file WeekViewViewModel.swift.
Replace the import statement for Foundation with an import statement for UIKit and declare the WeekViewViewModel
struct.
WeekViewViewModel.swift
1
import
UIKit
2
3
struct
WeekViewViewModel
{
4
5
}
You already know what the next step is. We need to create a property for the weather data the view model manages. We name the property weatherData
and the property is of type array of WeatherDayData
instances.
WeekViewViewModel.swift
1
import
UIKit
2
3
struct
WeekViewViewModel
{
4
5
//
MARK:
- Properties
6
7
let
weatherData
:
[
WeatherDayData
]
8
9
}
The WeekViewViewModel
struct will look a bit different from the DayViewViewModel
struct because it manages an array of model objects. Once we’re done refactoring the WeekViewController
class, it will no longer have a reference to the weather data. This means that the week view controller won’t know how many sections and rows the table view should display. That information needs to come from the view controller’s view model.
Let’s start by adding a new computed property to the WeekViewViewModel
struct, numberOfSections
. The numberOfSections
computed property returns the number of sections the week view controller should display in the table view it manages. The view controller currently display only one section, which means we return 1
.
WeekViewViewModel.swift
1
var
numberOfSections
:
Int
{
2
return
1
3
}
The view controller also needs to know how many rows that section contains. To answer this question, we add another computed property, numberOfDays
, which tells the view controller the number of days the view model has weather data for. We could implement a fancy method that accepts an index corresponding with the section in the table view, but I prefer to keep view models as simple and as dumb as possible. The view controller and its view model should only be given information they absolutely need to carry out their tasks.
WeekViewViewModel.swift
1
var
numberOfDays
:
Int
{
2
return
weatherData
.
count
3
}
Up until now, we used computed properties to provide the view controller with the information it needs. It’s time for a few methods. These methods will provide the view controller with weather data for a particular day. This means that the view controller asks the view model for the weather data for a specific index. The index corresponds with a row in the table view.
Let’s start with the day label of the WeatherDayTableViewCell
. The view controller will ask its view model for a string that represents the day of the weather data. This means we need to implement a method in the WeekViewViewModel
struct that accepts an index of type Int
and returns a value of type String
.
WeekViewViewModel.swift
1
func
day
(
for
index
:
Int
)
->
String
{
2
3
}
We first fetch the WeatherDayData
instance that corresponds with the index that’s passed to the day(index:)
method. We create a DateFormatter
instance and format the value of the time
property of the model.
WeekViewViewModel.swift
1
func
day
(
for
index
:
Int
)
->
String
{
2
// Fetch Weather Data for Day
3
let
weatherDayData
=
weatherData
[
index
]
4
5
// Initialize Date Formatter
6
let
dateFormatter
=
DateFormatter
()
7
8
// Configure Date Formatter
9
dateFormatter
.
dateFormat
=
"EEEE"
10
11
return
dateFormatter
.
string
(
from
:
weatherDayData
.
time
)
12
}
As I mentioned earlier in this book, I prefer to make the DateFormatter
instance a property of the view model. Let’s take care of that now. This is what the WeekViewViewModel
struct looks like so far.
WeekViewViewModel.swift
1
import
UIKit
2
3
struct
WeekViewViewModel
{
4
5
//
MARK:
- Properties
6
7
let
weatherData
:
[
WeatherDayData
]
8
9
//
MARK:
-
10
11
private
let
dayFormatter
=
DateFormatter
()
12
13
//
MARK:
-
14
15
var
numberOfSections
:
Int
{
16
return
1
17
}
18
19
//
MARK:
- Methods
20
21
func
day
(
for
index
:
Int
)
->
String
{
22
// Fetch Weather Data for Day
23
let
weatherDayData
=
weatherData
[
index
]
24
25
// Configure Date Formatter
26
dayFormatter
.
dateFormat
=
"EEEE"
27
28
return
dayFormatter
.
string
(
from
:
weatherDayData
.
time
)
29
}
30
31
}
We can use the same approach to populate the date label of the WeatherDayTableViewCell
. Only the value of the date formatter’s dateFormat
property is different. You could argue that we could get away with only one DateFormatter
property. That’s a personal choice. It won’t dramatically impact performance since we only have a handful of table view cells to populate.
WeekViewViewModel.swift
1
import
UIKit
2
3
struct
WeekViewViewModel
{
4
5
//
MARK:
- Properties
6
7
let
weatherData
:
[
WeatherDayData
]
8
9
//
MARK:
-
10
11
private
let
dayFormatter
=
DateFormatter
()
12
private
let
dateFormatter
=
DateFormatter
()
13
14
...
15
16
func
date
(
for
index
:
Int
)
->
String
{
17
// Fetch Weather Data for Day
18
let
weatherDayData
=
weatherData
[
index
]
19
20
// Configure Date Formatter
21
dateFormatter
.
dateFormat
=
"MMMM d"
22
23
return
dateFormatter
.
string
(
from
:
weatherDayData
.
time
)
24
}
25
26
}
Setting the text
property of the temperature label of the WeatherDayTableViewCell
is another fine example of the elegance and versatility of view models. Remember that the WeatherDayTableViewCell
displays the minimum and the maximum temperature for a particular day. The view model should provide the formatted string to the view controller so that it can pass it to the WeatherDayTableViewCell
.
In the temperature(for:)
method, we fetch the weather data, format the minimum and maximum temperatures using a helper method, format(temperature:)
, and return the formatted string as a result.
WeekViewViewModel.swift
1
func
temperature
(
for
index
:
Int
)
->
String
{
2
// Fetch Weather Data
3
let
weatherDayData
=
weatherData
[
index
]
4
5
let
min
=
format
(
temperature
:
weatherDayData
.
temperatureMin
)
6
let
max
=
format
(
temperature
:
weatherDayData
.
temperatureMax
)
7
8
return
"
\(
min
)
-
\(
max
)
"
9
}
The format(temperature:)
helper method isn’t complicated. It only prevents us from repeating ourselves.
WeekViewViewModel.swift
1
//
MARK:
- Helper Methods
2
3
private
func
format
(
temperature
:
Double
)
->
String
{
4
switch
UserDefaults
.
temperatureNotation
()
{
5
case
.
fahrenheit
:
6
return
String
(
format
:
"%.0f 째F"
,
temperature
)
7
case
.
celsius
:
8
return
String
(
format
:
"%.0f 째C"
,
temperature
.
toCelcius
())
9
}
10
}
Populating the wind speed label and the icon image view is very similar to what we covered so far. Take a look at the implementations below.
WeekViewViewModel.swift
1
func
windSpeed
(
for
index
:
Int
)
->
String
{
2
// Fetch Weather Data
3
let
weatherDayData
=
weatherData
[
index
]
4
let
windSpeed
=
weatherDayData
.
windSpeed
5
6
switch
UserDefaults
.
unitsNotation
()
{
7
case
.
imperial
:
8
return
String
(
format
:
"%.f MPH"
,
windSpeed
)
9
case
.
metric
:
10
return
String
(
format
:
"%.f KPH"
,
windSpeed
.
toKPH
())
11
}
12
}
WeekViewViewModel.swift
1
func
image
(
for
index
:
Int
)
->
UIImage
?
{
2
// Fetch Weather Data
3
let
weatherDayData
=
weatherData
[
index
]
4
5
return
UIImage
.
imageForIcon
(
withName
:
weatherDayData
.
icon
)
6
}
With the WeekViewViewModel
struct ready to use, we shift focus to the RootViewController
class. First, however, we replace the week
property in the WeekViewController
class with a property named viewModel
of type WeekViewViewModel?
.
WeekViewController.swift
1
var
viewModel
:
WeekViewViewModel
?
{
2
didSet
{
3
updateView
()
4
}
5
}
We can now update the RootViewController
class. The root view controller no longer passes the array of WeatherDayData
instances to the week view controller. Instead, the root view controller instantiates an instance of the WeekViewViewModel
using the array of WeatherDayData
instances and sets the viewModel
property of the week view controller.
Open RootViewController.swift and navigate to the fetchWeatherData()
method. In the completion handler of the weatherDataForLocation(latitude:longitude:completion:)
method, we create an instance of the WeekViewViewModel
struct and assign it to the viewModel
property of the week view controller.
RootViewController.swift
1
private
func
fetchWeatherData
()
{
2
...
3
4
dataManager
.
weatherDataForLocation
(
latitude
:
latitude
,
longitude
\
5
:
longitude
)
{
(
response
,
error
)
in
6
if
let
error
=
error
{
7
print
(
error
)
8
}
else
if
let
response
=
response
{
9
// Configure Day View Controller
10
self
.
dayViewController
.
viewModel
=
DayViewViewModel
(
weat
\
11
herData
:
response
)
12
13
// Configure Week View Controller
14
self
.
weekViewController
.
viewModel
=
WeekViewViewModel
(
we
\
15
atherData
:
response
.
dailyData
)
16
}
17
}
18
}
Revisit WeekViewController.swift. With the WeekViewViewModel
struct ready to use, it’s time to refactor the WeekViewController
class. This means we need to update the updateWeatherDataContainer(withWeatherData:)
method. We start by renaming this method to updateWeatherDataContainer()
. There’s no need to pass in the view model like we did in the DayViewController
class.
WeekViewController.swift
1
private
func
updateWeatherDataContainer
()
{
2
weatherDataContainer
.
isHidden
=
false
3
4
tableView
.
reloadData
()
5
}
We also update the updateView()
method to reflect these changes. We also replace any references to the week
property with references to the viewModel
property.
WeekViewController.swift
1
private
func
updateView
()
{
2
activityIndicatorView
.
stopAnimating
()
3
tableView
.
refreshControl
?.
endRefreshing
()
4
5
if
let
_
=
viewModel
{
6
updateWeatherDataContainer
()
7
8
}
else
{
9
...
10
}
11
}
The implementation of the UITableViewDataSource
protocol also needs some changes. As you can see, we use the methods we implemented earlier in the WeekViewViewModel
struct.
WeekViewController.swift
1
func
numberOfSections
(
in
tableView
:
UITableView
)
->
Int
{
2
guard
let
viewModel
=
viewModel
else
{
return
0
}
3
return
viewModel
.
numberOfSections
4
}
WeekViewController.swift
1
func
tableView
(
_
tableView
:
UITableView
,
numberOfRowsInSection
secti
\
2
on
:
Int
)
->
Int
{
3
guard
let
viewModel
=
viewModel
else
{
return
0
}
4
return
viewModel
.
numberOfDays
5
}
Thanks to the WeekViewViewModel
struct, we can greatly simplify the implementation of tableView(_:cellForRowAt:)
.
WeekViewController.swift
1
func
tableView
(
_
tableView
:
UITableView
,
cellForRowAt
indexPath
:
Ind
\
2
exPath
)
->
UITableViewCell
{
3
guard
let
cell
=
tableView
.
dequeueReusableCell
(
withIdentifier
:
W
\
4
eatherDayTableViewCell
.
reuseIdentifier
,
for
:
indexPath
)
as
?
WeatherD
\
5
ayTableViewCell
else
{
fatalError
(
"Unexpected Table View Cell"
)
}
6
7
if
let
viewModel
=
viewModel
{
8
// Configure Cell
9
cell
.
dayLabel
.
text
=
viewModel
.
day
(
for
:
indexPath
.
row
)
10
cell
.
dateLabel
.
text
=
viewModel
.
date
(
for
:
indexPath
.
row
)
11
cell
.
iconImageView
.
image
=
viewModel
.
image
(
for
:
indexPath
.
ro
\
12
w
)
13
cell
.
windSpeedLabel
.
text
=
viewModel
.
windSpeed
(
for
:
indexPat
\
14
h
.
row
)
15
cell
.
temperatureLabel
.
text
=
viewModel
.
temperature
(
for
:
inde
\
16
xPath
.
row
)
17
}
18
19
return
cell
20
}
Last but not least, we can get rid of the DateFormatter
properties of the WeekViewController
. They’re no longer needed and that’s a very welcome change.
Even though we successfully implemented the Model-View-ViewModel pattern in the week view controller, later in this book, we use protocols to further simplify the implementation of the Model-View-ViewModel pattern in the week view controller.