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

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.

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.

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