đź’ˇ This article is originally written on my Medium in 2018 and moved here in 2021

Image for post
MVC Diagram

MVC (Stands for Model-View-Controller) is a design pattern in software engineering world that has around for many years. It is always been good to make your code has separate layer between the Model, View and the Controller. In iOS World, MVC is the default design pattern, since Apple them self recommend this design pattern on their development ecosystem. When your app needs feature to save data to save to the device, or interact from the server, MVCS design pattern is good for your code, since it separates the “S” layer from the “C”. So, what is the “S” layer?

MVCS (MVC-Store/Service) is a design pattern that separate Database or networking interaction from the Controller. This pattern appear because the Controller handle so many different things, so it needs to be simplified. Some People says MVCN (N for networking). 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 MVCS design pattern.

Just focus on MVCS Alamofire Folder for now.

Layers

MVCS containts some different layer and its own responsibility, the Model, View, Controller, and Store/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.

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)
    }
}

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
            }
        }
    }
    
}

Controller

This Controller (or ViewController) is used for communicating between the Model, Service and View. It will tell the Service class to make request and display it to the Storyboard (View) view.

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: - View life cycle
    override func viewDidLoad() {
        super.viewDidLoad()
        attemptFetchPhoto(withId: 1)
    }
    
    // MARK: - Networking
    func attemptFetchPhoto(withId id: Int) {
        DataService.shared.requestFetchPhoto(with: id) {[weak self] (photo, error) in
            if let error = error {
                print("error: \(error.localizedDescription)")
                return
            }
            guard let photo = photo else {
                return
            }
            self?.updateUI(with: photo)
        }
    }
    
    // MARK: - UI Setup
    func updateUI(with photo: Photo) {
        print("photo: \(photo)")
        // change http to https with own extension function first
        if
            let urlString = photo.url,
            let finalUrlString = String.replaceHttpToHttps(with: urlString),
            let url = URL(string: finalUrlString),
            let title = photo.title,
            let albumId = photo.albumID
        {
            headerImageView.sd_setImage(with: url, completed: nil)
            titleLabel.text = title
            subtitleLabel.text = "This photo has albumID: \(albumId)"
        }
    }
}

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

You can see my full code here.

Conclusion

MVCS is a good design pattern for separating the the logic, because each of the component have their own responsibility, but MVC or MVCS is not always right. It depends on your app. Simple app will be fine using this pattern. If your app scale bigger, I think it’s time to move on to another design pattern such as MVVM (Model-View-ViewModel) or VIPER (View-Interactor-Presenter-Entity-Router).

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