When developing an app with SwiftUI, there are many architecture options to choose from.
Here, we will examine an example using MVVM-C.
- The app is a very simple example with three main features: a basic list, a filtering function for the list, and navigation to a detail screen.
- The code was written as first-party as much as possible.
- Test code is also included.
- Originally, MVVM includes network communication logic within the ViewModel. However, many programmers separate this logic into components named Repository or Service.
This example app also follows this approach and separates it under the name 'Network Service.' - If you want to focus on the architecture, please pay attention to the PokemonModule.
- architecture pattern may not be perfect and is not the definitive answer. I am a junior iOS developer with only two years of experience.
- Feel free to open an issue if there's something missing or you'd like to discuss further.
MVVM architecture has likely been the most popular since the UIKit days
and is commonly used in apps of moderate size.
The MVVM-C architecture extends the MVVM architecture, which consists of
Model-View-ViewModel, by introducing a Coordinator object responsible for
screen navigation, thus separating the responsibilities that were previously
handled by the View.
Let's take a look at what components there are.
Simply put, the Model represents the data.
It can be data received from external sources (e.g., data parsed from JSON received from a server)
or data created and stored within the app itself (e.g., data from CoreData).
The View is responsible for drawing the interface.
In UIKit, it also handled tasks like Delegate processing.
However, in SwiftUI, these responsibilities have been removed, allowing the View to focus more solely on rendering the interface.
Originally, the View was also responsible for screen navigation, but in this architecture, it is handled by the Coordinator, which will be discussed later.
Additionally, the View passes user events to an object called ViewModel, which will be discussed later, and it also displays the data provided by the ViewModel on the screen.
The roles of the ViewModel are as follows
- Process the data from the Model to prepare it for the View, meaning transforming it into data that will be displayed on the screen.
- Receive events from the View and perform the necessary actions.
- Execute business logic.
- Pass screen navigation events to the Coordinator.
It is the object that performs the above roles.
The Coordinator is responsible for managing screen navigation.
In traditional MVVM, this role was entirely handled by the View. However, if complex navigation logic is kept in the View, the code can become complicated and maintenance can be challenging.
Additionally, it becomes difficult to test. To address these drawbacks, the Coordinator is a separate object designed specifically for this purpose.
By adopting MVVM-C, developers can create more modular, testable, and maintainable iOS applications, particularly beneficial for projects expected to grow in complexity over time.
- Clear Separation of Concerns:
Each component has a well-defined responsibility, making the codebase more organized and maintainable. - Improved Testability:
ViewModels and Coordinators can be easily unit tested without dependencies on UI components. - Scalability:
As the app grows, new features can be added without significantly impacting existing components. - Relatively easy:
Implementation pattern that is not too difficult, with a low learning curve.
While MVVM-C offers many benefits, it's important to consider its potential drawbacks
- Consistency Challenges:
Not all programmers employ the same MVVM-C pattern.
This means that different projects may use different variations of the MVVM-C pattern, resulting in some inconsistency. - In larger apps, complexity increases:
In more complex apps, all logic except for rendering the View or handling screen transitions may be offloaded to the ViewModel, potentially leading to a Massive ViewModel.
The View renders the UI and sends events to the ViewModel.
These events can be a button tap or an onAppear when the screen is displayed.
In the example app, this applies when the screen is shown or when the filter button is pressed.
The ViewModel receives events from the View and performs the corresponding logic.
In the example app, this involves making network requests to the Network Service to fetch List data or executing list filtering logic for the filter button.
The Network Service handles communication with external servers and converts JSON data into Model objects to be passed to the ViewModel.
The ViewModel processes the Model provided by the Network Service into data suitable for displaying in the View.
The name ViewModel might be derived from this concept: Model for the View = ViewModel.
The ViewModel does not know about the View. Therefore, the View updates the data to be displayed on its own.
This is done using various binding objects in SwiftUI.
In the example app, @Observable was used.
If the ViewModel receives an event related to screen navigation, it delegates this to the Coordinator.
The Coordinator then provides the appropriate View to the app to facilitate screen navigation.
- Model is responsible for handling data.
- The View is only responsible for rendering.
- Any other logic is handled by the ViewModel.
Considering the potential bloat of the ViewModel,
it's possible to create multiple objects like a Coordinator or NetworkService. However, if the ViewModel still becomes too large and maintaining it becomes difficult, it may be necessary to adopt a more advanced architecture such as Clean Architecture or RIBs.
- The ViewModel does not know about the View and does not instruct it to display data.
Instead, the View automatically updates based on the data in the ViewModel.
Although you can use frameworks like Rx or Combine for this, SwiftUI provides its own tools like @Observable to handle such updates.
- In MVVM, since the View and business logic (ViewModel) are separated, the ViewModel can be tested independently without involving any UI code.
This enhances the maintainability of the app and improves the efficiency of testing.