15 January 2016

Figure 1: MVC vs MVVM.MVVM is roughly the same as MVC with the addition of a data layer in the form of the View Model that takes over interacting with the Model layer to provide data for presentation by the View layer.
Figure 1: MVC vs MVVM. MVVM is roughly the same as MVC with the addition of a data layer in the form of the View Model that takes over interacting with the Model layer to provide data for presentation by the View layer.

MVVM and MVC are generalizations of real-world architectures. In the real world, an app’s architecture may not exactly fit a single paradigm. Layers indicate a separation of responsibility and a model architecture can be adapted as necessary to a specific application.

I like to think of MVC and MVVM as representing the minimal amount of architectural separation necessary to manage a functioning app. With experience, you will create your own distinct layers that go beyond these fundamental paradigms to manage the features in your app.

For example, you might need to add a data synchronization layer or a network caching layer. Management of external resources is something that you’ll find falls outside of these model application architectures.

There is no requirement to use MVVM if you are using RAC4. It just happens that RAC integrates well with an MVVM architecture. This is one of the the reasons, I believe, that MVVM has become associated with RAC4.

I’d like to share my insights into MVVM and how it can be implemented in Swift with ReactiveCocoa 4. I provide code excerpts from a working demonstration app to serve as a concrete example of an actual implementation.

About MVVM

MVVM fits the same format as MVC with the difference being that an additional data layer, known as the View Model (VM), is added to the architecture. Figure 1 illustrates this viewpoint of mine.

All of the data that is to be shown in the UI first goes through the View Model instead of going directly to a view controller. The VM also takes over some of the functions of the controller layer in a standard MVC setup. In iOS and OS X, the view controller still serves to manage views. Since the VM is the primary change away from MVC, I’m going to focus most of my discussion on it.

Benefits of the View Model

Due to the data now being handled by the VM, instead of being handled by view controllers, the controller layer no longer needs to access the model layer. This reduces the responsibility of view controllers such that they only need to be concerned about handling views.

Overall, this results in a greater balance in the distribution of functions amongst the different code layers. The benefit of this is clearer code offering the possibility of easier maintenance. In contrast, with MVC, view controllers tend to get bloated with data handling functions and view controlling functions making them difficult to manage. With MVVM, view controllers tend to be reduced in size.

All of this talk gives you an idea of what to expect. Let’s look at some code that actually implements an MVVM architecture.

A common problem scenario

To keep things relevant, the problem that I’ll address in my example is having an app that fetches photos from a backend. This scenario is representative of many network-backed apps where RAC4 can offer significant advantages due to reducing complexity in handling compound asynchronous operations.

This situation highlights common problems that developers face with regard to user experience and performance. Photos are often not ready for immediate display due to the need for them to be retrieved from the network. The time delay imposed by these retrievals can have a negative effect on app performance if any of these operations block a critical thread such as the main UI thread. There can also be a visible delay where the user can see the photo loading into place. This may detract from the overall user experience of your app.

Additionally, with respect to your source code, the number of nested callbacks via completion handlers can increase to the point where it makes the code heavy and difficult to manage and understand.

An MVVM architecture in combination with ReactiveCocoa 4 serves to provide solutions to all of these problems by

  • Reducing the complexity of asynchronous operations by offering a streamlined paradigm for handling them via RAC4.
  • Maintaining a consistent user experience by having data before it is needed or as fast as possible in the View Model.
  • Improving code organization by distributing functions more evenly than in MVC.

Let’s jump into making an MVVM-based mini app to display photos retrieved from a Parse backend.

It’s going to display random photos from Parse using a view that is updated from a View Model where the photos are fetched using ReactiveCocoa 4.


Making a View Model

The essential components of a VM in Swift include a class to encapsulate the data that will be used by the UI. This can be in the form of properties to hold the data that will be used for presentation. In my implementation, the properties in the view controller mirror the properties of the View Model making for a 1:1 correspondence.

Here is the outline for my View Model class in Swift.

 1 import Foundation
 2 import ReactiveCocoa
 3 
 4 protocol ViewModelUpdatable: class {
 5     func viewModelDidUpdate()
 6 }
 7 
 8 /// A View Model class that holds photos that are retrieved asynchronously 
 9 /// from a backend.
10 class ViewModel: NSObject {
11     static let sharedInstance = ViewModel()
12     
13     /// Used to update the UI via delegation.
14     weak var delegate: ViewModelUpdatable?
15     
16     /// Used to update the UI via KVO.
17     dynamic var somePhoto: UIImage?
18     
19     /// Load photos from a RAC4 signal.
20     func loadData() {
21         DataLayer.sharedInstance.photoSignalProducer()
22         .on(next: { next in
23             self.kvoPhoto = next
24 
25             if self.kvoPhotoArray.count >= self.photoLimit {
26                 self.kvoPhotoArray = []
27             }
28 
29             if let photo = next {
30                 self.kvoPhotoArray.append(photo)
31             }
32         }, completed: {
33             self.timer?.invalidate()
34         }).start()
35     }
36 }

In my code, I’ve included both a protocol and a dynamic property for the purpose of implementing a couple of different ways of letting my UI know what to display.

Two ways of updating the UI from the VM

Observation via KVO

My VM inherits from NSObject so that KVO can be used on Swift properties declared as dynamic. The reason for this is so that properties in the View layer can be bound to the properties in the View Model layer. When these properties are bound, changes in the View Model are automatically reflected in the user interface.

Delegation

An alternative method of having the UI updated by the View Model is to use delegation. This is the reason I have listed the ViewModelUpdatable protocol that includes a function that can be called any time the UI is to be updated. In this case, my view controller subclass would conform to the protocol and implement the delegate method. The UI would then be updated by calling delegate.viewModelDidUpdate() in the VM.

This method of delegation offers the most control over when to update the UI from the View Model.


A truly minimal view controller for the View layer

Instead of a view controller bloated with functions for handling data, MVVM offers the opportunity to have a truly minimal view controller by limiting the responsibility of the controller to only handling its views. All data is pre-processed for display by the VM layer.

In my example view controller below, I have incorporated two methods for updating the views from the View Model.

One method uses KVO as shown by my observer registrations and de-registrations along with the observeValueForKeyPath:ofObject:change:context: method to handle property changes as they occur.

The other method uses delegation where the controller is conforming to ViewModelUpdatable and implementing the required delegate method updateView. The delegate method can then be called whenever the user interface needs to be updated by the View Model.

 1 import UIKit
 2 
 3 class ViewController: UIViewController, ViewModelUpdatable {
 4     @IBOutlet weak var imageView: UIImageView!
 5     private var kvoContext: UInt8 = 1
 6     
 7     /// The View Model instance for this view controller.
 8     let vm = ViewModel.sharedInstance 
 9     
10     required init?(coder aDecoder: NSCoder) {
11         super.init(coder: aDecoder)
12         vm.delegate = self
13         vm.addObserver(self, forKeyPath: "kvoPhoto", 
14                        options: .New, context: &kvoContext)
15     }
16     
17     deinit { vm.removeObserver(self, forKeyPath: "kvoPhoto") }
18     
19     override func viewWillAppear(animated: Bool) {
20         super.viewWillAppear(animated)
21         updateView()
22     }
23     
24     /// Update the UI from the View Model using delegation in Swift.
25     func viewModelDidUpdate() { imageView.image = vm.kvoPhoto }
26     
27     /// Update the UI from the View Model using KVO in Swift.
28     override func observeValueForKeyPath(keyPath: String?, 
29                                          ofObject object: AnyObject?, 
30                                          change: [String : AnyObject]?, 
31                                          context: UnsafeMutablePointer<Void>) {
32         if context == &kvoContext {
33             if let chg = change {
34                 if keyPath == "kvoPhoto" {
35                   imageView.image = chg["new"] as? UIImage
36                 }
37             }
38         }
39     }
40     
41     func updateView() { imageView.image = vm.kvoPhoto }
42 }

A Model layer with a Parse backend

Finally, the data access layer corresponding to the Model in my MVVM architecture is implemented as a separate Swift class. It accesses the Parse.com backend to retrieve photos for the View Model.

Using RAC4 gives us some conveniences by allowing the declaration of a result type for the signal producer that includes both the photo result along with any errors that occur during retrieval.

A shouldShowPhoto() function is used to randomly choose a photo for display.

 1 import Foundation
 2 import ReactiveCocoa
 3 import Parse
 4 
 5 enum DataError: ErrorType {
 6     case parseQueryError(NSError?)
 7     case parseDataError(NSError?)
 8 }
 9 
10 class DataLayer {
11     static let sharedInstance = DataLayer()
12     var globalCounter = 0
13     var photoCount: UInt32 = 5
14 
15     func randomPhotoSignalProducer() -> SignalProducer<UIImage?, DataError> {
16         return SignalProducer { observer, disposable in
17             let query = PFQuery(className: "TestPhoto")
18             query.findObjectsInBackgroundWithBlock { objects, error in
19                 guard let objs = objects where error == nil else {
20                     observer.sendFailed(DataError.parseQueryError(error))
21                     return
22                 }
23                 var index = 0
24                 for obj in objs {
25                     if self.shouldShowPhoto(index, photoCount: photoCount) {
26                         if let photoFile = obj["photo"] {
27                             photoFile.getDataInBackgroundWithBlock { data, error in
28                                 guard let photoData = data where error == nil else {
29                                     observer.sendFailed(DataError.parseDataError(error))
30                                     return
31                                 }
32                                 observer.sendNext(UIImage(data: photoData))
33                             }
34                         }
35                         break
36                     } else { index++ }
37                 }
38             }
39         }
40     }
41 
42     func shouldShowPhoto(index: Int, photoCount: UInt32) -> Bool {
43         return index == Int(arc4random_uniform(photoCount))
44     }
45 }

In my example, I’ve using both the View Model and Model layer in static instances via their ‘sharedInstance’ properties. This was for the purpose of testing the use of the View Model as pre-fetched data cache. Separating data handling from the view controller enables a complete VM to be ready before the view needs to be displayed.

Summary

I’ve covered the main components of MVVM, illustrated in Figure 1 and implemented using Swift and ReactiveCocoa 4.

It’s the VM that separates MVVM from MVC. In my View Model, I’ve shown two methods of updating the user interface, observation via KVO and delegation. I’ve combined both of these methods in my example source code for the sake of comparison. Either method alone would be sufficient for getting data in the VM to be updated in the View layer. Using KVO requires that the VM inherits from NSObject and that properties are marked as dynamic.

In iOS and OS X, the View layer consists of the view controller and its views where the view controller has the limited responsibility of handling views instead of interfacing with the Model layer as it would in the MVC paradigm.

I’ve identified a few advantages in using an MVVM architecture implemented with RAC4.

  • Reducing the complexity of asynchronous operations using ReactiveCocoa.
  • Maintaining a consistent user experience by managing data in the VM.
  • Improving code organization by distributing functions more evenly than with MVC.

In my example, it can be seen that RAC4 is only used within the Model and View Model layers. This provides the added benefits of having the View layer be independent of more complex operations used to handle data and having data for views be testable without dependencies on the UI. In other words, an app could be tested without the explicit need for UI testing.

MVVM, as I see it it, is not much different from MVC. I’ve shown that it can be implemented primarily by creating a separate data layer in the form of a View Model. You may have already created such a layer, such as for a data cache, without ever considering it as being a View Model. Now that you know how simple it is to move between MVC and MVVM, I hope that you’ll have the flexibility to explore how each paradigm can apply to your apps.



blog comments powered by Disqus