MVVM (Model-View-ViewModel) is another design pattern that people used in mobile development, including on iOS. This pattern gains popularity amongs mobile developer since the “C” layer from MVC handle so many things. Some people jokes about Massive-View-Controller (MVC) because the ViewController not really a controller, it also handle the UI, networking, and others.

MVVM provides good separation responsibility to it. The Model and the View are still the same. The ViewModel layer handle some logic like UI logic, bussiness logic, communicate to database or network service, so the main act in this pattern is the ViewModel. In other word, the ViewModel is the middle man between the View and the Model. Since the Controller is a UIViewController (not pure controller), people believe it is a View, not the Controller. I will show you how this pattern applied in a simple networking project using Alamofire and SDWebImage.

Grouping

This is how my project grouping folder in MVVM design pattern.

Just focus on MVVMAlamofire Folder for now.

Layers

MVVM in this project containts some different layer and its own responsibility, the Model, View, ViewModel, and Service.

View

The view represent how the data will rendered to the view. The storyboard file for this case is responsible for this layer. Here, I create simple view that holds UIImageView and two UILabel to show it to the user. Also, hook up it to the ViewController so we can interact with the view components via code. the ViewController.swift file is also a View, just ignore the Controller word.

Simple View with UIImageView and 2 UILabels.

Model

We will fetch JSON API form this link and convert the data to this model. Here, I use quicktype.io to quickly make model from the JSON API.

import Foundation
import Alamofire
struct Photo: Codable {
let albumID: Int?
let id: Int?
let title: String?
let url: String?
let thumbnailURL: String?
enum CodingKeys: String, CodingKey {
case albumID = "albumId"
case id = "id"
case title = "title"
case url = "url"
case thumbnailURL = "thumbnailUrl"
}
}
// MARK: Convenience initializers
extension Photo {
init(data: Data) throws {
self = try JSONDecoder().decode(Photo.self, from: data)
}
init(_ json: String, using encoding: String.Encoding = .utf8) throws {
guard let data = json.data(using: encoding) else {
throw NSError(domain: "JSONDecoding", code: 0, userInfo: nil)
}
try self.init(data: data)
}
init(fromURL url: URL) throws {
try self.init(data: try Data(contentsOf: url))
}
func jsonData() throws -> Data {
return try JSONEncoder().encode(self)
}
func jsonString(encoding: String.Encoding = .utf8) throws -> String? {
return String(data: try self.jsonData(), encoding: encoding)
}
}
// MARK: – Alamofire response handlers
extension DataRequest {
fileprivate func decodableResponseSerializer<T: Decodable>() -> DataResponseSerializer<T> {
return DataResponseSerializer { _, response, data, error in
guard error == nil else { return .failure(error!) }
guard let data = data else {
return .failure(AFError.responseSerializationFailed(reason: .inputDataNil))
}
return Result { try JSONDecoder().decode(T.self, from: data) }
}
}
@discardableResult
fileprivate func responseDecodable<T: Decodable>(queue: DispatchQueue? = nil, completionHandler: @escaping (DataResponse<T>) -> Void) -> Self {
return response(queue: queue, responseSerializer: decodableResponseSerializer(), completionHandler: completionHandler)
}
@discardableResult
func responsePhoto(queue: DispatchQueue? = nil, completionHandler: @escaping (DataResponse<Photo>) -> Void) -> Self {
return responseDecodable(queue: queue, completionHandler: completionHandler)
}
}
view raw Photo.swift hosted with ❤ by GitHub

Store/Service

For this example, I use Service rather than Store, since I’m just communicating to the network. This class is responsible to make a network call and give the result to the controller.

import Foundation
import Alamofire
struct DataService {
// MARK: – Singleton
static let shared = DataService()
// MARK: – URL
private var photoUrl = "https://jsonplaceholder.typicode.com/photos"
// MARK: – Services
func requestFetchPhoto(with id: Int, completion: @escaping (Photo?, Error?) -> ()) {
let url = "\(photoUrl)/\(id)"
Alamofire.request(url).responsePhoto { response in
if let error = response.error {
completion(nil, error)
return
}
if let photo = response.result.value {
completion(photo, nil)
return
}
}
}
}

ViewModel

Like I said earlier, the ViewModel is the middle man between the View and the Model. it makes request to the DataService layer, formatting data and represent it to the View layer (Controller), hold the loading state, and other. To make it easy, it’s just the Model for the View. The public properties and public closures are used by the ViewController to listen to any changes in the ViewModel, just like the delegate pattern in iOS.

import Foundation
class PhotoViewModel {
// MARK: – Properties
private var photo: Photo? {
didSet {
guard let p = photo else { return }
self.setupText(with: p)
self.didFinishFetch?()
}
}
var error: Error? {
didSet { self.showAlertClosure?() }
}
var isLoading: Bool = false {
didSet { self.updateLoadingStatus?() }
}
var titleString: String?
var albumIdString: String?
var photoUrl: URL?
private var dataService: DataService?
// MARK: – Closures for callback, since we are not using the ViewModel to the View.
var showAlertClosure: (() -> ())?
var updateLoadingStatus: (() -> ())?
var didFinishFetch: (() -> ())?
// MARK: – Constructor
init(dataService: DataService) {
self.dataService = dataService
}
// MARK: – Network call
func fetchPhoto(withId id: Int) {
self.dataService?.requestFetchPhoto(with: id, completion: { (photo, error) in
if let error = error {
self.error = error
self.isLoading = false
return
}
self.error = nil
self.isLoading = false
self.photo = photo
})
}
// MARK: – UI Logic
private func setupText(with photo: Photo) {
if let title = photo.title, let albumId = photo.albumID, let urlString = photo.url {
self.titleString = "Title: \(title)"
self.albumIdString = "Album ID for this photo : \(albumId)"
// formatting url from http to https
guard let formattedUrlString = String.replaceHttpToHttps(with: urlString), let url = URL(string: formattedUrlString) else {
return
}
self.photoUrl = url
}
}
}

Controller / ViewController

Like I said earlier, the ViewController or Controller belongs to the View layer. the code shoud like this.

import UIKit
import SDWebImage
class ViewController: UIViewController {
// MARK: – Outlet
@IBOutlet weak var headerImageView: UIImageView!
@IBOutlet weak var titleLabel: UILabel!
@IBOutlet weak var subtitleLabel: UILabel!
// MARK: – Injection
let viewModel = PhotoViewModel(dataService: DataService())
// MARK: – View life cycle
override func viewDidLoad() {
super.viewDidLoad()
attemptFetchPhoto(withId: 8)
}
// MARK: – Networking
private func attemptFetchPhoto(withId id: Int) {
viewModel.fetchPhoto(withId: id)
viewModel.updateLoadingStatus = {
let _ = self.viewModel.isLoading ? self.activityIndicatorStart() : self.activityIndicatorStop()
}
viewModel.showAlertClosure = {
if let error = self.viewModel.error {
print(error.localizedDescription)
}
}
viewModel.didFinishFetch = {
self.titleLabel.text = self.viewModel.titleString
self.subtitleLabel.text = self.viewModel.albumIdString
self.headerImageView.sd_setImage(with: self.viewModel.photoUrl, completed: nil)
}
}
// MARK: – UI Setup
private func activityIndicatorStart() {
// Code for show activity indicator view
//
print("start")
}
private func activityIndicatorStop() {
// Code for stop activity indicator view
//
print("stop")
}
}

The ViewController (View) communicates to the ViewModel, and listen to it using closures.

Build and run and you should see the data that you requested.

You can see my full code here.

Conclusion

MVVM is a better design pattern to avoid Massive View Controller, since the UI logic, networking / database and formatting the data done in the ViewModel layer. It is also make easier to test, rather than the MVC pattern. This pattern got many attention from the developer. If you still use MVC it is a big project, I think it is time to adopt to this pattern, MVVM.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s