Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 12

Migrating from MVC to MVVM: A Refactoring Guide

Recommended resources

Welcome to the final lesson of our module on Modern Architecture with MVVM!

So far, we have explored the core principles of MVVM, learned how to create reactive data flows using Combine, and, in our last lesson, mastered how to use Dependency Injection to make our ViewModels testable and modular. You now have all the individual components of a professional MVVM implementation.

Today, we will put everything together. Our goal is to refactor a feature from a legacy Model-View-Controller (MVC) project into the MVVM architecture. This is a highly practical and common task in the life of an iOS developer, where you'll often be asked to modernize and improve existing codebases. This lesson will serve as a capstone, applying all the concepts from this module to a realistic scenario.

Let's begin.

The "Massive View Controller" Problem

Before we start refactoring, it's important to understand why we're doing it. In iOS development, the standard MVC pattern can often lead to a problem colloquially known as "Massive View Controller," where the UIViewController becomes a bloated monolith responsible for too many things: UI logic, data fetching, state management, and business logic.

Introduction to MVVM: The 'Massive-View-Controller'

The article 'Introduction to MVVM' from AppCoda provides an excellent description of this common anti-pattern and sets the stage for our refactoring.

Please read the section titled 'Massive-View-Controller (MVC?)'. Pay attention to the arguments about why large view controllers are difficult to test and maintain.

Now, let's look at a concrete example of an app built with this traditional MVC approach.

Swift: How to Migrate MVC to MVVM & Intro Unit Testing

The video 'Swift: How to Migrate MVC to MVVM' by Lets Build That App starts by introducing a simple app built with MVC. It's a perfect example of the kind of code we often need to refactor.

Watch the segment from 00:46 to 06:48. As you watch, identify the signs of a 'Massive View Controller' and misplaced logic: Who owns the data? Notice that CoursesController has a courses property holding the app's state. Where is the presentation logic? Look inside the CourseCell's set course property observer. See the if statement that decides what text and accessory icon to show based on the numberOfLessons. This is presentation logic living inside the View.

The key issues here are clear:

  • The CoursesController is tightly coupled to the data fetching and holds the array of Course models.
  • The CourseCell is not just a dumb view; it contains logic to interpret the Course model and format its own appearance. This makes both the Controller and the View difficult to test and reuse.

The Refactoring Process: Systematically Moving to MVVM

Refactoring isn't about rewriting everything at once. It's a disciplined process of making small, verifiable changes. Our strategy will be to methodically extract logic from the View Controller and View and place it into a new ViewModel layer.

Step 1: Create the ViewModel and Update the Controller

First, we'll introduce a CourseViewModel. Its job is to take a Course model and transform it into a set of display-ready properties for the view. We'll then update our CoursesController to work with this new ViewModel.

Swift: How to Migrate MVC to MVVM & Intro Unit Testing

Let's continue with the video. The next segment shows how to create the CourseViewModel and adjust the CoursesController.

Watch from 06:48 to 11:12. Focus on these key actions: A new CourseViewModel struct is created. It uses initializer injection (init(course: Course)) to receive its dependency, the Course model—a pattern we covered in our last lesson. The CoursesController's courses array is changed to courseViewModels to hold [CourseViewModel]. Inside the fetchData completion handler, the array of Course models is transformed into an array of CourseViewModels using .map.

At this stage, we've introduced the ViewModel layer and made the Controller responsible for creating and holding these ViewModels. However, the app's UI is now broken because the ViewModel only exposes the name. Our next step is to move the presentation logic.

Step 2: Move Presentation Logic from the View to the ViewModel

The real power of MVVM comes from making the ViewModel responsible for all presentation logic. The View should become a "dumb" component that simply displays the data it's given.

Swift: How to Migrate MVC to MVVM & Intro Unit Testing

Now for the most critical part of the refactor. We will move the formatting and decision-making logic out of the CourseCell and into the CourseViewModel.

Watch from 11:12 to 17:52. Observe how the presentation logic is migrated: New properties (detailTextString, accessoryType) are added to the CourseViewModel. The if/else logic that was previously in the CourseCell is moved into the CourseViewModel's initializer. The ViewModel now determines the correct text and accessory type when it is created. The CourseCell is simplified. It now has a courseViewModel property and simply assigns the ViewModel's properties directly to its UI elements (textLabel.text, accessoryType, etc.).

Take a moment to appreciate the result. The CourseCell is now truly a "dumb" view. It has no knowledge of the underlying Course model or any of the business rules. It simply displays whatever the CourseViewModel tells it to. This clean separation of concerns is the primary goal of MVVM.

Xcode Project Structure: MVVM Refactoring from MVC
This image shows a well-organized Xcode project after an MVVM refactoring. Notice the clear separation of concerns into folders for Controllers, Models, Services, and importantly, View Models. This structure is the tangible result of the process we are following.

The Payoff: Verifiable Logic Through Unit Testing

We've invested effort in refactoring our code. What's the main benefit? Testability.

Before, testing the logic for displaying the "30+ lessons" text would have required instantiating a CourseCell and inspecting its UI properties—a slow and fragile UI test. Now, that logic lives in a simple, isolated CourseViewModel struct. We can test it directly with a fast, reliable unit test.

Swift: How to Migrate MVC to MVVM & Intro Unit Testing

The final part of the video demonstrates the payoff by writing unit tests for our new CourseViewModel.

Watch from 17:52 to 32:29. This is a longer segment, but it's crucial for understanding the 'why' behind MVVM. Pay attention to how the tests are constructed: A test target is set up. A Course model is created with a specific numberOfLessons (e.g., 10). A CourseViewModel is initialized with this Course model. XCTAssertEqual is used to assert that the ViewModel's properties (detailTextString, accessoryType) have the correct values for that specific input. A second test case is created with a different input (numberOfLessons = 100) to verify the other branch of the logic.

This ability to independently instantiate a ViewModel and verify its output for a given input is the hallmark of a testable architecture. You can now guarantee your presentation logic works correctly without ever running the simulator or touching the UI layer.


Conclusion

Congratulations on completing this module! You have successfully walked through one of the most important refactoring tasks in modern iOS development. By transforming a "Massive View Controller" into a clean MVVM architecture, you've created a codebase that is more organized, maintainable, and highly testable.

Key Takeaways:

  • Refactoring MVC to MVVM is a systematic process. It involves identifying misplaced logic in Views and View Controllers and moving it to a dedicated ViewModel.
  • The ViewModel acts as a transformer, converting raw model data into display-ready properties for the View.
  • Initializer Injection is a clean way to provide the Model to the ViewModel.
  • The View becomes "dumb", responsible only for displaying data provided by the ViewModel, not for interpreting it.
  • The most significant benefit of this separation is greatly improved testability, allowing you to write fast and reliable unit tests for your presentation logic.

Preview of the Next Lesson:
We've successfully cleaned up the responsibilities within a single screen. But what about managing the navigation between screens? This is another common task that clutters view controllers. In our next module, we'll address this by exploring another powerful design pattern. Our first lesson will be: Explain the role of the Coordinator pattern in managing navigation and decoupling view controllers.