MVVM -> Avoiding the “Imperative Soup”

The Setup

After working with MVC and MVP in cocoa touch for the last few years, I think it’s time to take a step back and consider the MVVM approach. Recently, I watched a talk given my Ben DiFrancesco where he discusses how and why you should adopt MVVM in your projects. Ben explains how adding target/action, delegation, notifications, KVO, and blocks(closures in Swift) to your ViewControllers really just creates this “imperative soup” where it’s complicated to reason about your business logic and other code. Additionally, it becomes harder to test and target your code for other platforms because you’re tightly coupled to platform specific frameworks. Speaking from experience, this definitely becomes a problem when you want to start adding extensions to your app; making sure all of your business logic works on Today, Watch, and other extensions and your main app can be a nightmare if the implementation isn’t handled with care.

Recap of MVC

Screen Shot 2016-07-29 at 4.05.23 PM

With MVC in iOS the controller has shared responsibilities; presentation logic is is mixed with business logic. This creates the imperative soup that Ben talks about: “The end result is code that’s hard reason about, hard to test, and [generally] ugly.”

Let’s take a look at how MVVM attempts to solve this problem.

Overview of MVVM

Screen Shot 2016-07-29 at 4.36.09 PM

With MVVM, we introduce a new object called the ViewModel that encapsulates presentation logic. The ViewModel consumes a model or group of models and creates properties that are pre-formatted for presentation on the view. The example that Ben gives are formatting strings, determining subview colors, setting show/hide booleans, or determining the number of rows in a table view. With this pattern, the ViewController’s job becomes a lot simpler because it’s just connecting ViewModel Properties to view properties.

Example App

In the spirit of learning new things, we’re going to put our MVVM skills to the test by making an example app. Imagine you’ve just been hired for a new role at a photograph agency and they’ve tasked you with making an app to showcase company’s directory of agents and their portfolios. They’ve decided on the name Portfolious, because it did well in hip/trendy focus groups. Anyways, in Portfolious, you’ll have a list of agents with data pertinent to them and a portfolio of their work. For your starting point, you’ll be using a subset of this JSON Placeholder data and a hip new networking library I’ve been turned on to recently. You can checkout the complete project on Github.

Application Layout

Using Sketch, we’ll create a view to showcase the theme of Portfolious. We’ll have a table view of agents where each cell will have some basic information like their name. Once you tap into an agent, you’ll be given an expanded set of their details (provided by the user model). Below that, we’ll have a table view of their albums where each album will present a collection view of the photos in that album.

Data Models

For our data source, we’ll be using three primary models: User, Album, and Photo. Each user will be an employee in the directory and we’ll include an album of their favorite photos when you go into their profile.

Users are defined as such:

{
"id": 1,
"name": "Leanne Graham",
"username": "Bret",
"email": "Sincere@april.biz",
"address": {
"street": "Kulas Light",
"suite": "Apt. 556",
"city": "Gwenborough",
"zipcode": "92998-3874",
"geo": {
"lat": "-37.3159",
"lng": "81.1496"
}
},
"phone": "1-770-736-8031 x56442",
"website": "hildegard.org",
"company": {
"name": "Romaguera-Crona",
"catchPhrase": "Multi-layered client-server neural-net",
"bs": "harness real-time e-markets"
}
}

Here are albums:

{
"userId": 1,
"id": 1,
"title": "quidem molestiae enim"
}

And photos within those albums:

{
"albumId": 1,
"id": 1,
"title": "accusamus beatae ad facilis cum similique qui sunt",
"url": "http://placehold.it/600/92c952",
"thumbnailUrl": "http://placehold.it/150/30ac17"
}

To get access to these JSON models, we’re going to use this networking library we mentioned earlier. To add it to our project, we’re going to use Carthage. Add the following line to your Cartfile in the top-level of your project directory.

github "KevinVitale/ReactiveJSON" "master"

Next, run the following command in your project directory to get the framework built.

carthage update --platform iOS

Drag and drop the frameworks produce by Carthage to your Embedded Binaries area of your main target.

Screen Shot 2016-07-30 at 8.44.36 AM
Screen Shot 2016-07-30 at 8.45.51 AM

And add this run phase script with the frameworks as input files.

Screen Shot 2016-07-30 at 8.51.20 AM

If we run the project we should get a blank screen, but our frameworks were added successfully.

Adding Models

The first thing we’re going to do is create some data models that our application will work with. Each model is a struct with properties based of the JSON we’re getting from JSON Placeholder. Each model conforms to the equatable protocol and implements its own == function.

API Client Controller

We’re going to add a network client called JSONPlaceholderClient.swift and it’s gaining to contain some code to connect to the JSONPlaceholder service

import Foundation
import ReactiveJSON

public struct JSONPlaceholder: JSONService, ServiceHostType {
private static let _sharedInstance = InstanceType()
//--------------------------------------------------------------------------
// protocol: JSONService
public typealias InstanceType = JSONPlaceholder
public static func sharedInstance() -> InstanceType {
return _sharedInstance
}
//--------------------------------------------------------------------------
// protocol: ServiceHostType
public static var scheme: String { return "http" }
public static var host: String { return "jsonplaceholder.typicode.com" }
public static var path: String? { return nil }
//--------------------------------------------------------------------------
}

Now that we have some code to talk to our remote JSON service, we can start thinking about how we can add JSON promising for our models. We’ll be using Argo to take the JSON we receive so we can encode it into our models. To add Argo, Curry, and Runes(Curry and Runes are Argo dependencies) into the project using Carthage, we need to add these lines to our Cartfile:

github "thoughtbot/Argo"
github "thoughtbot/Runes"
github "thoughtbot/Curry"

and run

carthage update --platform iOS

to build the schemes.

Once they’re built, we need to add them to our embedded binaries like we did before with ReactiveJSON.

Next we need to add Argo to our embedded binaries and run script like we did before. If everything worked, we should be able to import Argo into models to allow for JSON decoding.

extension User: Decodable {
static func decode(json: JSON) -> Decoded<User> {
return curry(self.init)
<^> json <| "id"
<*> json <| "name"
<*> json <| "username"
<*> json <| "email"
<*> json <| "address"
<*> json <| "phone"
<*> json <| "website"
<*> json <| "company"
}
}

Binding Example

For binding, we’re going to use plain old delegation with protocols. When we get new data from our network resources, we’re going to parse the response, use argo to build promised models that are guaranteed to be the objects we want, and finally send the ViewModel back to the ViewController so things like table/collection views can reload the data.

Here’s an example setup for the Photos collection view:

//
// AlbumPhotosViewModel.swift
// Portfolious
//
// Created by Andrew Sowers on 7/31/16.
// Copyright © 2016 Andrew Sowers. All rights reserved.
//
import Foundation
import ReactiveJSON
import Argo
import Result
import UIKit
protocol AlbumPhotosViewModelDelegate: class {
func didUpdateWith(viewModel viewModel: AlbumPhotosViewModel)
}
struct AlbumPhotosViewModel {
var album: Album
var photos: [Photo] = [Photo]() {
didSet {
delegate?.didUpdateWith(viewModel: self)
}
}
var navigationTitle: String {
return album.title
}
weak var delegate: AlbumPhotosViewModelDelegate?
init(album: Album) {
self.album = album
}
mutating func getPhotos() {
JSONPlaceholder
.request(endpoint: "photos/?albumId=\(album.id)")
.collect()
.startWithResult { (result: Result<[AnyObject], NetworkError>) in
switch result {
case .Success(let photos):
var newPhotos = [Photo]()
photos.forEach({
if let photo = Photo.decode(JSON($0)).value {
newPhotos.append(photo)
}
})
self.photos = newPhotos
case .Failure(let error):
print("Error: \(error)")
}
}
}

func imageView(forIndexPath indexPath: NSIndexPath, andImageView imageView: UIImageView?) {
imageView?.imageFromServerURL(photos[indexPath.row].url)
}
    func cell(forIndexPath indexPath: NSIndexPath, onCollectionView collectionView: UICollectionView) -> AgentPhotoCollectionViewCell {
        let cell = collectionView.dequeueReusableCellWithReuseIdentifier(MainStoryboard.ReuseIdentifiers.AgentPhotoCell, forIndexPath: indexPath) as! AgentPhotoCollectionViewCell
        imageView(forIndexPath: indexPath, andImageView: cell.imageView)
return cell
    }
}

and the ViewController

//
// AgentAlbumCollectionViewController.swift
// Portfolious
//
// Created by Andrew Sowers on 7/30/16.
// Copyright © 2016 Andrew Sowers. All rights reserved.
//
import UIKit
class AgentAlbumCollectionViewController: UICollectionViewController {
var viewModel: AlbumPhotosViewModel?
var album: Album?
override func viewDidLoad() {
super.viewDidLoad()
collectionView?.delegate = self
collectionView?.delegate = self
if let album = album {
viewModel = AlbumPhotosViewModel(album: album)
viewModel?.delegate = self
viewModel?.getPhotos()
}
}
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
navigationItem.title = viewModel?.navigationTitle ?? "Photos"
}
}
//MARK- CollectionView delegation
extension AgentAlbumCollectionViewController {
override func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return viewModel?.photos.count ?? 0
}
override func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) {
print("did the thing at \(indexPath)")
}
override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
return viewModel!.cell(forIndexPath indexPath: NSIndexPath, onCollectionView collectionView: UICollectionView)
}
}
//MARK- ViewModel delegation
extension AgentAlbumCollectionViewController: AlbumPhotosViewModelDelegate {
func didUpdateWith(viewModel viewModel: AlbumPhotosViewModel) {
dispatch_async(dispatch_get_main_queue()) { [weak self] in
self?.viewModel = viewModel
self?.collectionView?.reloadData()
}
}
}

When the data comes back from the ViewModel, we’re making a slight deviation from our networking library we used earlier and instead using this UIImageView extension that asynchronously downloads the image data from the Photo model.

//
// ImageFromServerURL.swift
// Portfolious
//
// Created by Andrew Sowers on 7/31/16.
// Copyright © 2016 Andrew Sowers. All rights reserved.
//
// Bluntly copped from https://stackoverflow.com/questions/37018916/swift-async-load-image
//
import UIKit
extension UIImageView {
public func imageFromServerURL(urlString: String) {

NSURLSession.sharedSession().dataTaskWithURL(NSURL(string: urlString)!, completionHandler: { (data, response, error) -> Void in

if error != nil {
print(error)
return
}
dispatch_async(dispatch_get_main_queue(), { () -> Void in
let image = UIImage(data: data!)
self.image = image
})

}).resume()
}
}

Wrapping up

Wow, it’s been quite a ride. We’ve taking a look at some design, Carthage, ReactiveJSON, MVVM, JSON promising with Argo, and things are looking almost good enough to ship. Going from here we would probably want to think of how we could optimize when we get resources from the network by caching results. Additionally, we might want to add some placeholder images for the albums and agent portfolios. Finally, we might want to add some unit tests if we decide to take Portfolious to market so we can be sure we’re shipping great production code.

Complete Project

You can find the complete project for this post on Github and I thank you for sticking with me on this journey!

Originally published at Haiku Robot.

One clap, two clap, three clap, forty?

By clapping more or less, you can signal to us which stories really stand out.