Introduction
There are many different architectures out there for your app, the most widely used in iOS development being Model View Controller(MVC). Although MVC is often now referred to jokingly as Massive View Controller because of its lack of abstraction. This has led to people looking into different approaches. We’re going to look into how you can use (Model-View-ViewModel) MVVM in iOS Applications. For more information on MVVM go to this Wikipedia page.
Using MVVM allows us to take some of the presentation logic out of the view controller. It allows us to create customized models for each view (you can still reuse the ViewModel for other views). For example, we may get a string from the API: "Hello World"
but we may always want to display this string as "Hello World, how are you?"
. It wouldn’t make sense to make this change in every view so this is where ViewModels come into their own.
Prerequisites
- Xcode 9.3+
- Swift 4.1+
- A JSON file – Example JSON
- Some knowledge of iOS programming.
We’re going to be building upon the “Swift 4 decoding JSON using Codable” tutorial that I wrote. Use this project as your starter project. Remember when using the starter project to change the development team to your own within the general settings.
Main Content
Setting up your folder structure
It’s important when using MVVM to make sure we have the correct folder structure, this makes your project more maintainable. With your base group “WeatherForecast” highlighted:
- File → New → Group
- Rename group to be “ViewModel”
- Repeat but name group “Controller”
- Drag your
ViewController.swift
to sit within the new “Controller” group.
Creating your view model
Our view controller (VC) currently retrieves a CurrentWeather
object from the model, that looks like this:
struct CurrentWeather: Codable {
let coord: Coord
let weather: [WeatherDetails]
let base: String
let main: Main
let visibility: Int
let wind: Wind
let clouds: Clouds
let dt: Int
let sys: Sys
let id: Int
let name: String
}
It contains other objects as well as high-level objects like strings and ints. We can use a ViewModel to extract out the information we actually want for this VC. For example, this VC may only be interested in displaying wind information, the coordinates, and the name of this CurrentWeather
object.
Let’s start by creating the new ViewModel.
- Highlight your “ViewModel” group
- File → New → File
- Select Swift File and press Next
- Name it “WindViewModel” and press Create
Within your newly created file add the following code:
// ViewModel/WindViewModel.swift
import Foundation
struct WindViewModel {
let currentWeather: CurrentWeather
init(currentWeather: CurrentWeather) {
self.currentWeather = currentWeather
}
}
Here we are creating our new WindViewModel
struct and providing a custom initializer so that we can pass in our CurrentWeather
object from the view controller. This view model will then handle the manipulation of the data within the current weather object to how we wish to display it. This takes the logic away from the view controller.
Next, we need to add all the properties that we are interested in using within the view. It’s important here to make sure that we don’t add more than necessary. Add the following below your currentWeather
declaration.
private(set) var coordString = ""
private(set) var windSpeedString = ""
private(set) var windDegString = ""
private(set) var locationString = ""
Take note of the private setter, this means that the variable can be read outside of the file but can only be modified from within it. We now need to set these properties. Your class should look like this when complete.
// ViewModel/WindViewModel.swift
import Foundation
struct WindViewModel {
let currentWeather: CurrentWeather
private(set) var coordString = ""
private(set) var windSpeedString = ""
private(set) var windDegString = ""
private(set) var locationString = ""
init(currentWeather: CurrentWeather) {
self.currentWeather = currentWeather
updateProperties()
}
//1
private mutating func updateProperties() {
coordString = setCoordString(currentWeather: currentWeather)
windSpeedString = setWindSpeedString(currentWeather: currentWeather)
windDegString = setWindDirectionString(currentWeather: currentWeather)
locationString = setLocationString(currentWeather: currentWeather)
}
}
extension WindViewModel {
//2
private func setCoordString(currentWeather: CurrentWeather) -> String {
return "Lat: \(currentWeather.coord.lat), Lon: \(currentWeather.coord.lon)"
}
private func setWindSpeedString(currentWeather: CurrentWeather) -> String {
return "Wind Speed: \(currentWeather.wind.speed)"
}
private func setWindDirectionString(currentWeather: CurrentWeather) -> String {
return "Wind Deg: \(currentWeather.wind.deg)"
}
private func setLocationString(currentWeather: CurrentWeather) -> String {
return "Location: \(currentWeather.name)"
}
}
- Creating a mutating function allows us to change the properties of the struct.
- Create separate functions for each property.
These functions are pretty simple but they could get more complicated in the future especially if we start using optional values within the Current Weather object. If we were using MVC and had to access an optional value in many places within our VC we could see the size of that class grow very quickly and filled with guard statements. Whereas using MVVM allows us to use that guard statement once per optional. For example, if the location variable was optional we could do something like this:
private func setLocationString(currentWeather: CurrentWeather) -> String {
guard let name = currentWeather.name else {
return "Location not available"
}
return "Location: \(name)"
}
We can then keep using the locationString
variable within our view controller wherever required without checking if it is nil or not because we have already handled our error case.
Setting and using your view model
Now that we have created our view model structure we need to create and set an object that we can use within our view controller. Within the ViewController.swift
class below the
private let apiManager = APIManager()
line add the following code:
// Controller/ViewController.swift
private(set) var windViewModel: WindViewModel?
var searchResult: CurrentWeather? {
didSet {
guard let searchResult = searchResult else { return }
windViewModel = WindViewModel.init(currentWeather: searchResult)
}
}
This creates a WindViewModel
object that we can set within the view controller, we also have a CurrentWeather
object that contains a didSet
property observer. This means that when the property is set or altered the code within this observer will be run. In our case, it will create a new WindViewModel
passing in the CurrentWeather
object and setting it to our classes WindViewModel
variable. Now, all we need to do is set the searchResult
variable when we get our callback from the API.
Modify the getWeather()
method within our VCs extension so that it looks like this:
// Controller/ViewController.swift
private func getWeather() {
apiManager.getWeather() { (weather, error) in
if let error = error {
print("Get weather error: \(error.localizedDescription)")
return
}
guard let weather = weather else { return }
self.searchResult = weather
print("Current Weather Object:")
print(weather)
}
}
We’ve now set our ViewModel. We can now add some UI elements to check that the VC is doing what we expect it to. Open the Main.storyboard
and delete the label that says Check Console for Output as we won’t be needing that.
- Add four new labels to your ViewController.
- Make sure they are big enough to view your content. (Full width of the screen should be big enough for this).
- Optionally you can set your constraints as well.
Add the following outlets to your VC and hook them up to your nearly created labels.
// Controller/ViewController.swift
@IBOutlet weak var locationLabel: UILabel!
@IBOutlet weak var windSpeedLabel: UILabel!
@IBOutlet weak var windDirectionLabel: UILabel!
@IBOutlet weak var coordLabel: UILabel!
Our final two steps are to create a function that can edit the text within these labels and finally to call that function. First, create the following function within the ViewController extension.
// Controller/ViewController.swift
private func updateLabels() {
guard let windViewModel = windViewModel else { return }
locationLabel.text = windViewModel.locationString
windSpeedLabel.text = windViewModel.windSpeedString
windDirectionLabel.text = windViewModel.windDegString
coordLabel.text = windViewModel.coordString
}
Remember that windViewModel
is declared as optional so we need to make sure that it has been set before we can access any of the properties. This is why we have a guard at beginning of the function. We now have to call this function within our didSet
of the searchResult
variable by modifying the variable so that it looks like this:
// Controller/ViewController.swift
var searchResult: CurrentWeather? {
didSet {
guard let searchResult = searchResult else { return }
windViewModel = WindViewModel.init(currentWeather: searchResult)
DispatchQueue.main.async {
self.updateLabels()
}
}
}
You should now be able to launch the app and press the Get Data button and see the labels change to what you set within your ViewModel. We could go to our ViewModel and change the wind speed string to contain an “m/s” unit at the end. This would then mean that any view using this ViewModel would have the unit changed with us only having to change it within one place.
Conclusion
MVVM allows use to abstract logic away from the ViewController so we can keep ViewControllers as simple as possible. It also allows you to reuse code across many view controllers much easier than traditional MVC. It does need some extra setup before you can start using your objects. This means that for very simple projects it can be seen as overkill. You should take into account when deciding upon the architecture of your app whether MVVM will be beneficial to you in the long run.