YTII Official Blog - Stories, Updates and Tips

MVC Vs MVVM - Exploring iOS App Design Patterns

Written by Achin Verma | July 14, 2023 11:40:06 AM Z

When it comes to developing iOS applications using Swift, choosing the right architecture pattern is crucial. Two popular choices are Model-View-Controller (MVC) and Model-View-ViewModel (MVVM). In this article, we will explore these architectural patterns, understand their differences, and discuss when and how to use each one.

What is MVC?

Model-View-Controller (MVC) is an architecture pattern widely used in iOS development. It provides a structured way of organizing code by separating it into three distinct components: the Model, the View, and the Controller.

    • The Model represents the data and business logic of the application. It encapsulates the data structures, algorithms, and interactions with the database or network.
    • The View is responsible for presenting the user interface to the user. It displays the data and communicates user interactions back to the controller.
    • The Controller acts as an intermediary between the Model and the View. It receives input from the View, processes it, updates the Model if necessary, and updates the View accordingly.

When to use MVC?

MVC is a good choice for small to medium-sized projects with a relatively simple user interface and a straightforward data flow. It provides a clear separation of concerns and can be a good starting point for beginners due to its simplicity.

Looking to bring your iOS app idea to life? Hire our skilled iOS app developers today and turn your vision into a stunning reality. Get in touch now to start building your next successful app!

How to use MVC?

To implement MVC in a Swift project, follow these guidelines:

    • Define your model classes and structs that represent the data and business logic.
    • Create your view classes or storyboards that define the user interface.
    • Implement your controller classes that handle user input and orchestrate the interaction between the model and the view.

Ensure that the responsibilities of each component are well-defined, and avoid placing excessive business logic in the controller.

Let's see everything in action; we will create an app that will show a list of posts. We will be using the free API from Jsonplaceholder

Our files directory will look like this

Here all the UI-related files, such as storyboards, xibs will be in the View folder. All the ViewControllers will be in the controller folder. Drag a UI viewController and add tableView on it. Connect the outlets and create a table view cell. Here, I have created a table view cell with the name of PostCell.

Our app will communicate with a server to fetch the posts, So to make a network request create an extension of URLSession

NOTE: For the sake of simplicity, we do not delve deeper into the networking part. You can write the networking logic the way you want. I have created a simple method that will fetch the posts from the server.

extension URLSession {

func perform<T: Decodable>(_ request: URLRequest, decode decodable: T.Type, result: @escaping (Result<T, Error>) -> Void) {

URLSession.shared.dataTask(with: request) { (data, response, error) in

// handle error scenarios... `result(.failure(error))`
// handle bad response... `result(.failure(responseError))`
// handle no data... `result(.failure(dataError))`
guard let data = data else {return}
do {
let object = try JSONDecoder().decode(decodable, from: data)
result(.success(object))
} catch {
result(.failure(error))
}

}.resume()

}

}

Now, Our networking logic is set up, its time to create a Model. API response has an array of dictionaries, and each dictionary is represented as Post

So, To create a model, we will use the Decodable protocol (Assuming you know about this protocol)

We will create a new file PostModel under the Model folder.

import Foundation

struct Posts : Decodable{
var userId : Int?
var id : Int?
var title : String?
var body : String?
}

Now we will create an object of this Post model type in ViewController file

private var postArr = [Posts]()

In view, the controller creates a table view outlet and conforms to the delegate and data source methods. Now when the user lands on this screen, we need to make a network request that will fetch the latest posts from the server. To do that, let's create a method in ViewController

 private func fetchPosts(){
let url = URL(string: "https://jsonplaceholder.typicode.com/posts")!
let request = URLRequest(url: url)

URLSession.shared.perform(request, decode: [Posts].self) { [self] (result) in
switch result {
case .failure(let error):
print(error.localizedDescription)
case .success(let object):
print(object)
postArr = object
DispatchQueue.main.async {
self.tblview.reloadData()
}

}
}
}

Here fetchPosts method is using our URLsession extension method to make a request. We are decoding the data and checking if it was successfully decoded or not. If it succeeds, we are replacing the empty postArr to an object (decoded posts array) and reload the table view.

The complete code of the view controller should look like this

import UIKit

class ViewController: UIViewController {

@IBOutlet weak var tblview: UITableView!

private var postArr = [Posts]()

override func viewDidLoad() {
super.viewDidLoad()
tblview.delegate = self
tblview.dataSource = self
tblview.tableFooterView = UIView()
fetchPosts()
}

private func fetchPosts(){
let url = URL(string: "https://jsonplaceholder.typicode.com/posts")!
let request = URLRequest(url: url)

URLSession.shared.perform(request, decode: [Posts].self) { [self] (result) in
switch result {
case .failure(let error):
print(error.localizedDescription)
case .success(let object):
print(object)
postArr = object
DispatchQueue.main.async {
self.tblview.reloadData()
}

}
}
}

}

extension ViewController : UITableViewDelegate,UITableViewDataSource{

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return postArr.count
}

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "PostCell") as! PostCell
cell.titleLbl.text = postArr[indexPath.row].title ?? ""
cell.desc.text = postArr[indexPath.row].body ?? ""
return cell
}

func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableView.automaticDimension
}

}

Now run the app, and you will see the list of posts fetched from the server; here, your controller directly talks with the model, and once the new data is fetched, the controller requests the views to update themselves. Here you can see we have written the business logic directly into the controller, which makes total lines of code increased and tightly coupled.

What is MVVM?

Model-View-ViewModel (MVVM) is another architecture pattern gaining popularity in iOS development. It builds upon MVC by introducing a new component called the ViewModel.

    • The ViewModel is responsible for exposing the data and commands that the View needs to display and handle user interactions. It abstracts the view-specific logic away from the Model and provides an interface to bind the View to the data.

When to use MVVM?

MVVM is suitable for larger projects with complex user interfaces, as well as projects that require extensive unit testing. It promotes better separation of concerns and allows for easier testing of the ViewModel’s logic in isolation.

Ready to take your mobile app idea to the next level? Hire our experienced mobile app developers and turn your vision into a reality. Contact us now to discuss your project and start building the app of your dreams!

How to use MVVM?

To implement MVVM in a Swift project, follow these steps:

    1. Define your model classes or structs as in MVC.
    1. Create your view classes or storyboards as in MVC.
    1. Implement your ViewModel classes, which expose properties and methods that the View needs to bind to. The ViewModel should also handle user interactions and update the Model accordingly.

To establish the connection between the View and the ViewModel, you can use bindings or reactive frameworks like Combine or ReactiveSwift.

Let’s see everything in action; we will use the same app and logic but power it using the MVVM pattern. We will be using the same MODEL file and the same VIEWS. Our files directory will look like this

In MVVM, we create the Model and Views in the same way we did in MVC. The only thing that we have to take care of here is creating a ViewModel, and establishing a connection between ViewModel and the controller (Remember, in MVC, the controller directly communicated with a model, in MVVM controller communicates with ViewModel, and viewModel communicates with the Model)

Let's create a file PostVM under ViewModel. Create a class called PostVM and move the postArr object and fetchPosts method from viewController to PostVM. It will throw a xcode error on the code of the line where we have reloaded our tableview, remove that line.

Now we have moved our fetching logic to View model, we need some type of mechanism that will pass the information back to viewController so that table view can reload. To do that lets create a Protocol with two methods

protocol PostVMProtocol : AnyObject {
func didGetError(error:String)
func didGetPost()
}

Create an object of PostVMProtocol type in PostVM.

weak var delegate : PostVMProtocol!

Now in our fetchPosts method call these methods using delegate object, call the error method where decoding fails and didGetPost when decoded successfully. Our PostVM will look like this

import Foundation

protocol PostVMProtocol : AnyObject {
func didGetError(error:String)
func didGetPost()
}

final class PostVM{

var postArr = [Posts]()
weak var delegate : PostVMProtocol!

func fetchPosts(){
let url = URL(string: "https://jsonplaceholder.typicode.com/posts")!
let request = URLRequest(url: url)

URLSession.shared.perform(request, decode: [Posts].self) { [self] (result) in
switch result {
case .failure(let error):
print(error.localizedDescription)
delegate.didGetError(error: error.localizedDescription)
case .success(let object):
print(object)
postArr = object
DispatchQueue.main.async { [self] in
delegate.didGetPost()
}

}
}
}

}

Now, in viewController create an object viewmodel of PostVM type

private var viewmodel = PostVM()

Assign the delegate to self and call the fetchPosts method of PostVM in viewDidLoad of viewController

  viewmodel.delegate = self
viewmodel.fetchPosts()

Create an extension, add those two protocols methods in viewController and reload the table view in didGetPost()

extension ViewController : PostVMProtocol{
func didGetError(error: String) {
print(error)
}

func didGetPost() {
self.tblview.reloadData()
}
}

In tableview delegates and datasources now we can use the posts array that is in PostVM, Use the viewmodel object to call the postArr from postVm

extension ViewController : UITableViewDelegate,UITableViewDataSource{

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return viewmodel.postArr.count
}

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "PostCell") as! PostCell
cell.titleLbl.text = viewmodel.postArr[indexPath.row].title ?? ""
cell.desc.text = viewmodel.postArr[indexPath.row].body ?? ""
return cell
}

func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableView.automaticDimension
}

}

Our viewController will look like this now

import UIKit

class ViewController: UIViewController {

@IBOutlet weak var tblview: UITableView!

private var viewmodel = PostVM()

override func viewDidLoad() {
super.viewDidLoad()
tblview.delegate = self
tblview.dataSource = self
tblview.tableFooterView = UIView()
viewmodel.delegate = self
viewmodel.fetchPosts()
}

}

extension ViewController : UITableViewDelegate,UITableViewDataSource{

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return viewmodel.postArr.count
}

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "PostCell") as! PostCell
cell.titleLbl.text = viewmodel.postArr[indexPath.row].title ?? ""
cell.desc.text = viewmodel.postArr[indexPath.row].body ?? ""
return cell
}

func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableView.automaticDimension
}

}

extension ViewController : PostVMProtocol{
func didGetError(error: String) {
print(error)
}

func didGetPost() {
self.tblview.reloadData()
}

}

Now run the app, and you will see the same list of posts, but behind the walls, we have used MVVM & if you compare the viewController in MVC & MVVM, you will see that with MVVM it's much smaller and clean. We have moved all the business logic into ViewModel, and it can be easily tested. (To do the unit testing we have to use initializers to make it loosely coupled and testable)


ViewController in MVC
ViewController in MVC

Download the complete source code

Conclusion

Both MVC and MVVM are viable architecture patterns for developing iOS applications in Swift. The choice between the two depends on the specific requirements of the project.

MVC is a simpler pattern that works well for small to medium-sized projects with straightforward user interfaces. It provides a clear separation of concerns but can become challenging to maintain as the project grows.

On the other hand, MVVM offers better separation of concerns, making it suitable for larger projects with complex UIs. It facilitates unit testing and provides a more scalable solution.

In the end, it’s essential to consider factors such as project size, complexity, maintainability, and testing requirements when selecting an architecture pattern. By understanding the differences between MVC and MVVM, you can make an informed decision and create well-structured Swift applications that are easier to maintain and extend.

Frequently Asked Questions

What is MVC and MVVM?
MVC (Model-View-Controller) and MVVM (Model-View-ViewModel) are architectural design patterns used in iOS app development. MVC separates an app into three components: model, view, and controller, while MVVM adds a ViewModel layer between the view and the model.
What are the main differences between MVC and MVVM?
In MVC, the controller handles user input and updates the view accordingly, while in MVVM, the ViewModel acts as a mediator between the view and the model, managing data binding and providing a clean separation of concerns.
What are the advantages of using MVC?
MVC provides a clear separation of responsibilities, making it easier to understand and maintain code. It is a well-known and widely-used pattern with ample documentation and community support.
What are the advantages of using MVVM?
MVVM enhances testability and allows for better code reusability. It promotes a more declarative approach to UI development and enables easier integration of reactive programming frameworks.
When should I use MVC?
MVC is a good choice for small to medium-sized projects with straightforward requirements. It is suitable when the application's complexity is relatively low and the focus is on rapid development.
When should I use MVVM?
MVVM is beneficial for larger projects that require more extensive data binding and event-driven interactions. It is a good fit when the application's UI needs to be easily testable and when utilizing reactive programming frameworks.
Are there any drawbacks to using MVC?
MVC can lead to massive view controllers, making code maintenance challenging. It can also result in tight coupling between components, making it harder to introduce changes or modifications.
Are there any drawbacks to using MVVM?
Implementing MVVM may introduce additional complexity, especially for smaller projects. It requires learning and understanding new concepts and can have a steeper learning curve compared to MVC.
Can I mix elements of MVC and MVVM?
Yes, it is possible to mix elements of both patterns. You can incorporate MVVM concepts within an existing MVC architecture by introducing a ViewModel layer to handle data binding and separation of concerns.