Using MVVM in iOS

ios-tutorial.png

This tutorial examines how to take an MVVM in IOS development approach. You will add features to a provided demo app, following MVVM principles.

Introduction

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:

  • FileNewGroup
  • Rename group to be “ViewModel”
  • Repeat but name group “Controller”
  • Drag your ViewController.swift to sit within the new “Controller” group.
MVVM iOS Folder Structure

Creating your view model

Our view controller (VC) currently retrieves a CurrentWeather object from the model, that looks like this:

1struct CurrentWeather: Codable {
2        let coord: Coord
3        let weather: [WeatherDetails]
4        let base: String
5        let main: Main
6        let visibility: Int
7        let wind: Wind
8        let clouds: Clouds
9        let dt: Int
10        let sys: Sys
11        let id: Int
12        let name: String
13    }

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
  • FileNewFile
  • Select Swift File and press Next
  • Name it “WindViewModel” and press Create

Within your newly created file add the following code:

1// ViewModel/WindViewModel.swift
2    import Foundation
3    struct WindViewModel {
4
5        let currentWeather: CurrentWeather
6
7        init(currentWeather: CurrentWeather) {
8            self.currentWeather = currentWeather
9        }
10    }

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.

1private(set) var coordString = ""
2    private(set) var windSpeedString = ""
3    private(set) var windDegString = ""
4    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.

1// ViewModel/WindViewModel.swift
2    import Foundation
3    struct WindViewModel {
4
5        let currentWeather: CurrentWeather
6        private(set) var coordString = ""
7        private(set) var windSpeedString = ""
8        private(set) var windDegString = ""
9        private(set) var locationString = ""
10
11        init(currentWeather: CurrentWeather) {
12            self.currentWeather = currentWeather
13            updateProperties()
14        }
15
16        //1
17        private mutating func updateProperties() {
18            coordString = setCoordString(currentWeather: currentWeather)
19            windSpeedString = setWindSpeedString(currentWeather: currentWeather)
20            windDegString = setWindDirectionString(currentWeather: currentWeather)
21            locationString = setLocationString(currentWeather: currentWeather)
22        }
23
24    }
25    extension WindViewModel {
26        //2
27        private func setCoordString(currentWeather: CurrentWeather) -> String {
28            return "Lat: \(currentWeather.coord.lat), Lon: \(currentWeather.coord.lon)"
29        }
30
31        private func setWindSpeedString(currentWeather: CurrentWeather) -> String {
32            return "Wind Speed: \(currentWeather.wind.speed)"
33        }
34
35        private func setWindDirectionString(currentWeather: CurrentWeather) -> String {
36            return "Wind Deg: \(currentWeather.wind.deg)"
37        }
38
39        private func setLocationString(currentWeather: CurrentWeather) -> String {
40            return "Location: \(currentWeather.name)"
41        }
42    }
  • 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:

1private func setLocationString(currentWeather: CurrentWeather) -> String {
2      guard let name = currentWeather.name else {
3        return "Location not available"
4      }
5      return "Location: \(name)"
6    }

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:

1// Controller/ViewController.swift
2    private(set) var windViewModel: WindViewModel?
3    var searchResult: CurrentWeather? {
4        didSet {
5                guard let searchResult = searchResult else { return }
6                windViewModel = WindViewModel.init(currentWeather: searchResult)
7        }
8    }

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:

1// Controller/ViewController.swift
2    private func getWeather() {
3        apiManager.getWeather() { (weather, error) in
4            if let error = error {
5                print("Get weather error: \(error.localizedDescription)")
6                return
7            }
8            guard let weather = weather  else { return }
9            self.searchResult = weather
10            print("Current Weather Object:")
11            print(weather)
12        }
13    }

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.

1// Controller/ViewController.swift
2    @IBOutlet weak var locationLabel: UILabel!
3    @IBOutlet weak var windSpeedLabel: UILabel!
4    @IBOutlet weak var windDirectionLabel: UILabel!
5    @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.

1// Controller/ViewController.swift
2    private func updateLabels() {
3        guard let windViewModel = windViewModel else { return }
4        locationLabel.text = windViewModel.locationString
5        windSpeedLabel.text = windViewModel.windSpeedString
6        windDirectionLabel.text = windViewModel.windDegString
7        coordLabel.text = windViewModel.coordString
8    }

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:

1// Controller/ViewController.swift
2    var searchResult: CurrentWeather? {
3        didSet {
4            guard let searchResult = searchResult else { return }
5            windViewModel = WindViewModel.init(currentWeather: searchResult)
6            DispatchQueue.main.async {
7                self.updateLabels()
8            }
9        }
10    }

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

The completed project can be found here.

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.