How to implement MVVM pattern with Swift in iOS

Before we begin, let me explain with simple words what an architecture pattern (or design pattern) is.

When you start making iOS apps, you write your code in one file, usually in the View (UIViewController). But as you start learning more and more and adding new things into your app, you’ll start seeing that you have to scroll up and down these 2000 lines to understand what you wrote.

So this is where the architecture pattern comes to solve that problem! Using an architecture pattern, you can split this huge file into separate files to make it easier to maintain, test, and scale.

One of the architecture patterns we’ll implement today, it’s called MVVM. MVVM stands from Model – View – ViewModel.

In this tutorial, I’ll show you how to implement the MVVM architecture pattern and organize your project with folders.

We’re going to make an app that retrieves employees’ data from a JSON and displays them in a UITableView with custom UITableViewCell.

This is how the structure of the project will look like after organizing and implementing MVVM.

Creating the Folders

Let’s start by creating the folders Helpers, Services, and Screens.

After that, the your project will look like this:

Creating the MVVM Folders

The Screens folder contains all the screens of our app. So, for example, if you have a screen with the title ‘My Profile’, then, inside the Screens folder, create a new folder with the name MyProfile, and inside that folder, make the folders Model, View, and ViewModel.

Using this method, you’ll keep everything you want in the same place!

In this example, we have the screen Employees, and we moved the ViewController file inside the View and renamed it to EmployessViewController

Tip: If you use the same file in different places, you can create a Common folder and have the file inside there.

Creating the TableView Cell and the TableView

Before we implement the MVVM pattern, let’s make the UITableViewCell and UITableView.

• UITableViewCell

Inside the View folder, create a new folder with a name Cell, and inside this folder, create a UITableViewCell with the name EmployeeCell and a XIB file

In this .xib file, we have UILabels inside a UIStackView to display the IDNameSalary, and Age of an employee.

Now, inside the EmployeeCell.swift file, add the classes identifier, and nib.

These classes will help us later to register the cell in the UITableView without using hardcoded strings.

class EmployeeCell: UITableViewCell { @IBOutlet var idLabel: UILabel! @IBOutlet var nameLabel: UILabel! @IBOutlet var salaryLabel: UILabel! @IBOutlet var ageLabel: UILabel! class var identifier: String { return String(describing: self) } class var nib: UINib { return UINib(nibName: identifier, bundle: nil) } override func awakeFromNib() { super.awakeFromNib() initView() } func initView() { // Cell view customization backgroundColor = .clear // Line separator full width preservesSuperviewLayoutMargins = false separatorInset = UIEdgeInsets.zero layoutMargins = UIEdgeInsets.zero } override func prepareForReuse() { super.prepareForReuse() idLabel.text = nil nameLabel.text = nil salaryLabel.text = nil ageLabel.text = nil } }
Code language: Swift (swift)

• UITableView

In the Main.storyboards add a UITableView inside a UIViewController

In the EmployeesViewController.swift file we add two methods, initView() and initViewModel().

Inside the initView(), we do all the things that have to do with the tableview’s customization and preparing, like background color, register cell e.t.c

In the initViewModel(), we create the closures from the ViewModel (We’re going to add them later in the tutorial).

class EmployeesViewController: UIViewController { @IBOutlet var tableView: UITableView! override func viewDidLoad() { super.viewDidLoad() initView() initViewModel() } func initView() { // TableView customization tableView.delegate = self tableView.dataSource = self tableView.backgroundColor = UIColor(#colorLiteral(red: 0.6196078431, green: 0.1098039216, blue: 0.2509803922, alpha: 1)) tableView.separatorColor = .white tableView.separatorStyle = .singleLine tableView.tableFooterView = UIView() tableView.allowsSelection = false tableView.register(EmployeeCell.nib, forCellReuseIdentifier: EmployeeCell.identifier) } func initViewModel() { /* Add code later */ } } // MARK: - UITableViewDelegate extension EmployeesViewController: UITableViewDelegate { func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { return 130 } } // MARK: - UITableViewDataSource extension EmployeesViewController: UITableViewDataSource { func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return 0 } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { guard let cell = tableView.dequeueReusableCell(withIdentifier: EmployeeCell.identifier, for: indexPath) as? EmployeeCell else { fatalError("xib does not exists") } return cell } }
Code language: Swift (swift)

Creating the Model for the JSON Data

In this example, we’re getting the ID, Name, Age, and Salary of the employees from a JSON. To decode that JSON data, we’re gonna need to create a model.

Inside the Model folder, create a new swift file with the name Employee

Tip: If your JSON is very complex and hard for you to create the model manually, use app.quicktype.io to generate it.

typealias Employees = [Employee] // MARK: - Employee struct Employee: Codable { let id: String let employeeName: String let employeeSalary: String let employeeAge: String enum CodingKeys: String, CodingKey { case id case employeeName = "employee_name" case employeeSalary = "employee_salary" case employeeAge = "employee_age" } }
Code language: Swift (swift)

Helpers

Now, let’s see the Helpers folder.

As the name says, this folder contains files that will help us write less code during the development.

In this example, we have a file with the name HttpRequestHelper. Inside this class, we have a GET method that takes as parameters a URL, the request parameters (If they exist), and an HTTP Header and returns the results with the escaping closure complete.

enum HTTPHeaderFields { case application_json case application_x_www_form_urlencoded case none } class HttpRequestHelper { func GET(url: String, params: [String: String], httpHeader: HTTPHeaderFields, complete: @escaping (Bool, Data?) -> ()) { guard var components = URLComponents(string: url) else { print("Error: cannot create URLCompontents") return } components.queryItems = params.map { key, value in URLQueryItem(name: key, value: value) } guard let url = components.url else { print("Error: cannot create URL") return } var request = URLRequest(url: url) request.httpMethod = "GET" switch httpHeader { case .application_json: request.setValue("application/json", forHTTPHeaderField: "Content-Type") case .application_x_www_form_urlencoded: request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") case .none: break } // .ephemeral prevent JSON from caching (They'll store in Ram and nothing on Disk) let config = URLSessionConfiguration.ephemeral let session = URLSession(configuration: config) session.dataTask(with: request) { data, response, error in guard error == nil else { print("Error: problem calling GET") print(error!) complete(false, nil) return } guard let data = data else { print("Error: did not receive data") complete(false, nil) return } guard let response = response as? HTTPURLResponse, (200 ..< 300) ~= response.statusCode else { print("Error: HTTP request failed") complete(false, nil) return } complete(true, data) }.resume() } }
Code language: Swift (swift)

Services

The Services (or Networking) folder contains the files with the requests we are sending to a server.

Here we create a file with the name EmployeesService, and we’re using the GET method from the HttpRequestHelper to create the request.

protocol EmployeesServiceProtocol { func getEmployees(completion: @escaping (_ success: Bool, _ results: Employees?, _ error: String?) -> ()) } class EmployeesService: EmployeesServiceProtocol { func getEmployees(completion: @escaping (Bool, Employees?, String?) -> ()) { HttpRequestHelper().GET(url: "https://raw.githubusercontent.com/johncodeos-blog/MVVMiOSExample/demo.json", params: ["": ""], httpHeader: .application_json) { success, data in if success { do { let model = try JSONDecoder().decode(Employees.self, from: data!) completion(true, model, nil) } catch { completion(false, nil, "Error: Trying to parse Employees to model") } } else { completion(false, nil, "Error: Employees GET Request failed") } } } }
Code language: Swift (swift)

ViewModel

In this folder, we have the files that do all the business logic and don’t have any UIKit references, so it’s very easy to test.

Inside the ViewModel folder, create two new swift files with the name EmployeeCellViewModel and EmployeesViewModel.

• EmployeeCellViewModel

In the file EmployeeCellViewModel, we create a struct with all the properties for the cell.

struct EmployeeCellViewModel { var id: String var name: String var salary: String var age: String }
Code language: Swift (swift)

• EmployeesViewModel

In this file, we start by initializing the EmployeeService.

class EmployeesViewModel: NSObject { private var employeeService: EmployeesServiceProtocol init(employeeService: EmployeesServiceProtocol = EmployeesService()) { self.employeeService = employeeService // ... }
Code language: Swift (swift)

Then create a new method getEmployees, to get the employees’ data from the JSON using the employeeService.

If the request was successful, we passing the JSON model to the fetchData method.

class EmployeesViewModel: NSObject { // ... func getEmployees() { employeeService.getEmployees { success, model, error in if success, let employees = model { self.fetchData(employees: employees) } else { print(error!) } } } // ... }
Code language: Swift (swift)
class EmployeesViewModel: NSObject { // ... var reloadTableView: (() -> Void)? var employees = Employees() var employeeCellViewModels = [EmployeeCellViewModel]() { didSet { reloadTableView?() } } func fetchData(employees: Employees) { self.employees = employees // Cache var vms = [EmployeeCellViewModel]() for employee in employees { vms.append(createCellModel(employee: employee)) } employeeCellViewModels = vms } // ... }
Code language: Swift (swift)

Inside the fetchData method, we’re looping through the items list, and we create a new list of the cell’s view model using the createCellModel method.

class EmployeesViewModel: NSObject { // ... func createCellModel(employee: Employee) -> EmployeeCellViewModel { let id = employee.id let name = employee.employeeName let salary = "$ " + employee.employeeSalary let age = employee.employeeAge return EmployeeCellViewModel(id: id, name: name, salary: salary, age: age) } // ... }
Code language: Swift (swift)

Lastly, we create another method and return the cell view model for the current IndexPath.

class EmployeesViewModel: NSObject { // ... func getCellViewModel(at indexPath: IndexPath) -> EmployeeCellViewModel { return employeeCellViewModels[indexPath.row] } }
Code language: Swift (swift)

View

Back in the View folder, at the EmployeesViewController file, add the ViewModel, call the getEmployees method, and create the reloadTableView closure to update the tableView when it is called.

class EmployeesViewController: UIViewController { @IBOutlet var tableView: UITableView! lazy var viewModel = { EmployeesViewModel() }() // ... func initViewModel() { viewModel.getEmployees() viewModel.reloadTableView = { [weak self] in DispatchQueue.main.async { self?.tableView.reloadData() } } } }
Code language: Swift (swift)

Next, in the numberOfRowsInSection, return the number of cell view models, and in the cellForRowAt, call the getCellViewModel method to get the view model of the current cell.

extension EmployeesViewController: UITableViewDataSource { func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return viewModel.employeeCellViewModels.count } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { guard let cell = tableView.dequeueReusableCell(withIdentifier: EmployeeCell.identifier, for: indexPath) as? EmployeeCell else { fatalError("xib does not exists") } let cellVM = viewModel.getCellViewModel(at: indexPath) cell.cellViewModel = cellVM return cell } }
Code language: Swift (swift)

Getting the ViewModel in the Cell

Back in the TableViewCell (EmployeeCell), once the cellViewModel receives the data, the labels get updated.

class EmployeeCell: UITableViewCell { @IBOutlet var idLabel: UILabel! @IBOutlet var nameLabel: UILabel! @IBOutlet var salaryLabel: UILabel! @IBOutlet var ageLabel: UILabel! class var identifier: String { return String(describing: self) } class var nib: UINib { return UINib(nibName: identifier, bundle: nil) } var cellViewModel: EmployeeCellViewModel? { didSet { idLabel.text = cellViewModel?.id nameLabel.text = cellViewModel?.name salaryLabel.text = cellViewModel?.salary ageLabel.text = cellViewModel?.age } } // ... }
Code language: Swift (swift)
You can find the final project here

If you have any questionsplease feel free to leave a comment below

Subscribe
Notify of
guest
2 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments

Get once a week my latest tutorials right in your inbox

Check your inbox to confirm your email