This article presents you how to apply the MVVM pattern in DummyMap application you have developed in previous tutorial. To do this, you’re going to refactor the FeedVC class by using Facade pattern to hidden complexity of binding UI process and MVVM to help slim down the feed view controller by delegating some view controller’s tasks to a view model. Finally you will have new code easier to read and smoother binding process.

Getting Started

MVVM(model-view-viewModel) is an architecture pattern that is an alternative to MVC(model-view-controller). The current version of FeedVC class is written in tradition MVC pattern, the image below illustrates the main components of the MVC pattern:

This pattern separates the UI into the Model that represents the application state, the View, which in turn is composed of UI controls, and a Controller that handles user interactions and updates the model accordingly.

The problem with MVC in project is that it’s quite confusing. The networking, persistent, binding data code are all placed in view controller class cause massive code. MVVM, sometimes referred to as presentation model, offers a way to organize code that solve these issues.

The core of this pattern is viewModel. This file holds the values to be presented in your view. The logic to format the values to be present is placed in the viewModel. In the context of current DummyMap app, it make a advantage that you can format a viewModel from multiple source of model. You will find out later in this article.

The relationships between the three components of the MVVM pattern are simpler than the MVC equivalents, following these strict rules:

  • The View has a reference to the ViewModel, but not vice-versa.
  • The ViewModel has a reference to the Model, but not vice-versa.

The aims of this part are to refactor feed tab with MVVM and move networking, persistent code to outside FeedVC class.

Improve loading process

Build and run starter project. You can see the problem with current loading process is that on the first launch, app is freezing and slow. The first launch, app have to load data from server and convert return json to Core Data object then present to UI. After first launch, app load data much faster because data is saved in Core Data, and app fetch data and present it. The following activity diagram show the current loading process:

To improve it, you’re going to use MVVM pattern. The improved process is presented in the following diagram:

In new process, the json data will be convert to Data Model and parsing json to Core Data object will implement in background on the first launch. This process make data display much faster and app runs smoother than old one.

To archive this, you’re going to use Facade pattern to refactor FeedVC class. Facade pattern defines an simpler interface to an complex subsystem. You will divide FeedVC into some subsystems. The following sequence diagram will present how to use Facade pattern in this case:

  1. FeedVC implements only view logic .
  2. DataManager is Facade class. It provides a interface to use another subsystems.
  3. ApiClient implements requesting server to get json data and parsing json to Core Data object.
  4. PersistenceManager implements Core Data tasks like parsing json, clean data, fetching.
  5. RestaurantDataModel stores restaurant’s information and will be use to create view model

PersistenceManager

First, you need to add new Data Model struct to hold the restaurant’s information. Go to File\New\File…, select iOS\Source\Swift File template and click Next. Name the file RestaurantDataModel and click Create to save the file. Add the following contents:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import Foundation

enum RestaurantType: String {
case hawk = "hawk"
case coffee = "coffee"
case fastfood = "fastfood"
case streettea = "streettea"
case na = "N/a"
}

struct RestaurantDataModel {
var name: String
var bio: String
var type: RestaurantType
var rate: Double
var latitude: Double
var longitude: Double


init(restaurant: Restaurant) {
name = restaurant.name ?? ""
bio = restaurant.bio ?? ""
if let restaurantType = restaurant.type {
type = RestaurantType(rawValue: restaurantType) ?? .na
} else { type = .na }
rate = restaurant.rate
latitude = restaurant.latitude
longitude = restaurant.longitude
}

init(dict: [String:Any]) {
name = dict["name"] as? String ?? ""
bio = dict["bio"] as? String ?? ""
if let restaurantType = dict["type"] as? String {
type = RestaurantType(rawValue: restaurantType) ?? .na
} else { type = .na }
rate = dict["rate"] as? Double ?? 0
latitude = dict["latitude"] as? Double ?? 0
longitude = dict["longitude"] as? Double ?? 0
}

}

Next, Go to File\New\File…, select iOS\Source\Swift File template and click Next. Name the file PersistenceManager and click Create to save the file. Add the following contents:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
import UIKit
import CoreData

class PersistenceManager: NSObject {

static let coreDataStack = CoreDataStack(modelName: "DummyMap")

class func parseJsonData(input: [[String:Any]]) {
coreDataStack.storeContainer.performBackgroundTask { (context) in
for dict in input {
insertNewShopEntity(dict: dict, context: context)
}
try! context.save()
coreDataStack.saveContext()

}
}
class func insertNewShopEntity(dict: [String:Any], context: NSManagedObjectContext) {
guard let entity = NSEntityDescription.entity(forEntityName: "Restaurant",
in: context) else { return }
let restaurant = Restaurant(entity: entity, insertInto: coreDataStack.managedContext)
restaurant.id = dict["id"] as? String
restaurant.name = dict["name"] as? String
restaurant.bio = dict["bio"] as? String
restaurant.type = dict["type"] as? String
restaurant.rate = dict["rate"] as? Double ?? 0
restaurant.latitude = dict["latitude"] as? Double ?? 0
restaurant.longitude = dict["longitude"] as? Double ?? 0
}
class func fetchRestaurantList(completion: @escaping ([Restaurant]) -> Void) {
let request = NSFetchRequest<Restaurant>(entityName: "Restaurant")
do {
let results = try coreDataStack.managedContext.fetch(request)
completion(results)
} catch let error as NSError {
print("Could not fetch \(error), \(error.userInfo)")
}
}
class func fetchRestaurantModelList(completion: @escaping ([RestaurantDataModel]) -> Void) {
fetchRestaurantList(completion: { (restaurants ) in
var result: [RestaurantDataModel] = []
for item in restaurants {
let model = RestaurantDataModel(restaurant: item)
result.append(model)
}
completion(result)
})
}
class func clearAllData() {
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "Restaurant")
let batchDeleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
do {
try coreDataStack.managedContext.execute(batchDeleteRequest)

} catch {
// Error Handling
}
}
}

You can realize they are the functions you use in FeedVC class. The parseJsonData(input: [[String:Any]]) method has been add the following block:

1
2
3
coreDataStack.storeContainer.performBackgroundTask { (context) in
// Code
}

The adding new Core data and saving is performed in background thread. It make sure UI is not freezing during the process like before.

The class func fetchRestaurantModelList(completion: @escaping ([RestaurantDataModel]) -> Void) allows directly fetch and covert Core data object to Data Model.

ApiClient

This class just implements request server to get Json data. Go to File\New\File…, select iOS\Source\Swift File template and click Next. Name the file ApiClient and click Create to save the file. Add the following contents:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import UIKit
import Alamofire

let mainURL = "https://my-json-server.typicode.com/quanrong88/Demo-repo/shops"

class ApiClient: NSObject {

class func getRestaurantList(completion: @escaping ([RestaurantDataModel]) -> Void) {
Alamofire.request(mainURL).responseJSON { response in
switch response.result {
case .success:
print("Validation Successful")
if let json = response.result.value as? [[String:Any]] {
var result: [RestaurantDataModel] = []
for item in json {
let model = RestaurantDataModel(dict: item)
result.append(model)
}
completion(result)
PersistenceManager.parseJsonData(input: json)
UserDefaults.standard.set(true, forKey: "previouslyLaunched")
}

case .failure(let error):
print(error)
}


}
}
}

DataManager

This class is Facade class. It use to class 2 below subsystems. Go to File\New\File…, select iOS\Source\Swift File template and click Next. Name the file DataManager and click Create to save the file. Add the following contents:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import UIKit

let dataChangedNotificationName = NSNotification.Name(rawValue: "dataHasBeenReloaded")

class DataManager: NSObject {
var dataSource: [RestaurantDataModel] = []
static let shareInstance = DataManager()
let previouslyLaunched = UserDefaults.standard.bool(forKey: "previouslyLaunched")
func loadData(completion: @escaping ([RestaurantDataModel]) -> Void) {
if !previouslyLaunched {
ApiClient.getRestaurantList(completion: { [unowned self] (dataModels) in
self.dataSource = dataModels
completion(dataModels)
})
} else {
PersistenceManager.fetchRestaurantModelList(completion: { [unowned self] (dataModels) in
self.dataSource = dataModels
completion(dataModels)
})
}
}
func reloadData(completion: @escaping ([RestaurantDataModel]) -> Void) {
PersistenceManager.clearAllData()
ApiClient.getRestaurantList(completion: { [unowned self] (dataModels) in
self.dataSource = dataModels
completion(dataModels)
NotificationCenter.default.post(name: dataChangedNotificationName, object: nil)
})
}
}

There are 2 methods in this class.

  • loadData() : It check if app is first load, It will call ApiClient to request server, otherwise it will call PersistenceManager to fetch Data Model.
  • reloadData() : It will delete all data in Core Data and call ApiClient* to request new data from server.

Refactoring feedVC

Firstly, change the tint color of navigation bar to make app look nicer. Open Main.storyboard, select Navigation Bar in FeedVC‘s Navigation Controller.

Then, open Attributes Inspector and change Bar tint field like following:

Secondly, add a Bar Button Item to FeedVC by drag-drop it from Object library to FeedVC navigation item in storyboard, select button and open Attributes Inspector, set System item to Refresh. Your FeedVC should look like this:

Thirdly, it’s time to create your feed view model. Go to File\New\File…, select iOS\Source\Swift File template and click Next. Name the file FeedViewModel and click Create to save the file. Add the following contents:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import Foundation
import UIKit

struct FeedViewModel {
var name: String
var type: RestaurantType
var rate: Double
var image: UIImage {
switch type {
case .hawk:
return #imageLiteral(resourceName: "bakery")
case .coffee:
return #imageLiteral(resourceName: "coffee")
case .fastfood:
return #imageLiteral(resourceName: "fastfood")
case .streettea:
return #imageLiteral(resourceName: "water")
default:
return #imageLiteral(resourceName: "food-icon")
}
}

init(dataModel: RestaurantDataModel) {
name = dataModel.name
type = dataModel.type
rate = dataModel.rate
}
}

Also, create detail view model with the same method in file DetailViewModel.swift:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import Foundation
import UIKit

struct DetailViewModel {
var name: String
var bio: String
var type: RestaurantType
var rate: Double
var backGround: UIImage {
switch type {
case .hawk:
return #imageLiteral(resourceName: "hawk-bg")
case .coffee:
return #imageLiteral(resourceName: "coffee-bg")
case .fastfood:
return #imageLiteral(resourceName: "fastfood-bg")
case .streettea:
return #imageLiteral(resourceName: "streettea-bg")
default:
return #imageLiteral(resourceName: "streettea-bg")
}
}


init(dataModel: RestaurantDataModel) {
name = dataModel.name
bio = dataModel.bio
type = dataModel.type
rate = dataModel.rate
}
}

The final step is refactoring FeedVC class. There are 2 sub-step

  • Remove all code related to connect to server and parsing Core data. You’ve completed it in previous numberOfSections
  • Using view model to become data source of collection view in FeedVC

After that, your FeedVC should look like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
import UIKit
import Alamofire
import PKHUD
import Cosmos
import CoreData

class FeedVC: UIViewController {

@IBOutlet weak var feedCollectionView: UICollectionView!

var feedData: [FeedViewModel] = []

override func viewDidLoad() {
super.viewDidLoad()

feedCollectionView.register(DemoCell.nib, forCellWithReuseIdentifier: DemoCell.identifier)
DataManager.shareInstance.loadData(completion: { [unowned self] (restaurantModel) in
for model in restaurantModel {
let viewModel = FeedViewModel(dataModel: model)
self.feedData.append(viewModel)
}
self.feedCollectionView.reloadData()
})

}

override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
feedCollectionView.collectionViewLayout.invalidateLayout()
}


// MARK: - Navigation

// In a storyboard-based application, you will often want to do a little preparation before navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "showDetail", let desVC = segue.destination as? RestaurantDetailVC, let content = sender as? RestaurantDataModel {
desVC.restaurant = DetailViewModel(dataModel: content)
}
}

@IBAction func refreshBtnClicked(_ sender: UIBarButtonItem) {
HUD.show(.progress)
DataManager.shareInstance.reloadData(completion: {(restaurantModel) in
self.feedData.removeAll()
for model in restaurantModel {
let viewModel = FeedViewModel(dataModel: model)
self.feedData.append(viewModel)
}
self.feedCollectionView.reloadData()
HUD.hide()
})
}


}
extension FeedVC: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let restaurant = DataManager.shareInstance.dataSource[indexPath.row]
performSegue(withIdentifier: "showDetail", sender: restaurant)
}
}

extension FeedVC: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return feedData.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: DemoCell.identifier, for: indexPath) as! DemoCell
let restaurant = feedData[indexPath.row]
cell.titleLbl.text = restaurant.name
cell.rateView.rating = restaurant.rate
cell.typeIcon.image = restaurant.image
return cell
}
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
}

extension FeedVC: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
switch UIDevice.current.orientation {
case .landscapeLeft, .landscapeRight:
return CGSize(width: collectionView.bounds.size.width / 3 , height: 150)
default:
return CGSize(width: collectionView.bounds.size.width, height: 60)
}
}

}

Open Main.storyboard, select refresh bar button in FeedVC, open Connections Inspector drag Send Action field to refreshBtnClicked() function. Now you have finished refactoring FeedVC.

Also, you refactor RestaurantDetailVC class by adding DetailViewModel*. Replace following code to the file:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import UIKit
import Cosmos

class RestaurantDetailVC: UIViewController {

@IBOutlet weak var bioTextView: UITextView!
@IBOutlet weak var nameLbl: UILabel!
@IBOutlet weak var ratingView: CosmosView!
@IBOutlet weak var bgImg: UIImageView!
var restaurant: DetailViewModel?

override func viewDidLoad() {
super.viewDidLoad()

if let input = restaurant {
bindingUI(input: input)
}
}

override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
// MARK: Ultilities function
func bindingUI(input: DetailViewModel) {
nameLbl.text = input.name
ratingView.rating = input.rate
bioTextView.text = input.bio
bgImg.image = input.backGround

}

}

Run and build project, you should see the result like this:

Conclusions

In this article, the project has been refactored with MVVM. Overall, the project works similar the starter project, you only add refresh function and change navigation bar tint color but you can see that app works much faster than before. Combine with Facade pattern , project has been divided to small sub-system that easier to maintain.

Here is the final project of this part.