Refactoring to Resilience: Transitioning from MVVM to Clean Architecture

Arun kumar pattanayak
8 min readApr 7, 2024

--

In the constantly changing world of software development, architects and programmers are always looking for ways to improve code maintainability, scalability and testability. The change from Model-View-ViewModel (MVVM) architecture to Clean Architecture is one such shift that is gaining ground.

Because MVVM can separate concerns and provides data binding support, it has become a prevalent method of organising code in most modern mobile as well as web applications. But while projects become more complex, developers face tight coupling between layers, challenges in testing, and the need to keep separation clear.

Contrarily, Clean Architecture focuses on dividing responsibilities, being independent from external frameworks and ensuring that the code can be tested. It provides specific instructions on how best to split up your code into distinct layers with defined roles leading to less error-prone designs which allow for better maintenance and expansion.

This blogs will take you through the process of moving from MVVM to Clean Architecture. Specifically, we will explain what clean architecture entails including its guiding principles; we will also outline its constituents as opposed to MVVM. Besides this, there are strategies that can be used for rewriting existing MVVM-based codes so that it follows principles of a clean architecture as well as making development smoother and easier maintenance.

{
"data": [
{
"id": "bitcoin",
"rank": "1",
"symbol": "BTC",
"name": "Bitcoin",
"supply": "18824943.0000000000000000",
"maxSupply": "21000000.0000000000000000",
"marketCapUsd": "1104472461633.1075176780327501",
"volumeUsd24Hr": "66566480087.0421125881533446",
"priceUsd": "58602.6054006234050000",
"changePercent24Hr": "0.3583330721187001",
"vwap24Hr": "57638.2622376795013008"
},
// More assets...
],
"timestamp": 1649297859407
}

We will work on above JSON response to mock our code.
https://docs.coincap.io/


struct AssetData: Codable {
let data: [Asset]
let timestamp: TimeInterval
}

struct Asset: Codable {
let id: String
let rank: String
let symbol: String
let name: String
let supply: String
let maxSupply: String
let marketCapUsd: String
let volumeUsd24Hr: String
let priceUsd: String
let changePercent24Hr: String
let vwap24Hr: String
}

This will be our model structure for the same.

import UIKit

class AssetListViewController: UIViewController {
@IBOutlet weak var collectionView: UICollectionView!

let viewModel = AssetListViewModel()

// Declare diffable data source
var dataSource: UICollectionViewDiffableDataSource<Section, Asset>!

enum Section {
case main
}

override func viewDidLoad() {
super.viewDidLoad()

collectionView.collectionViewLayout = createLayout()

// Initialize and configure diffable data source
dataSource = UICollectionViewDiffableDataSource<Section, Asset>(collectionView: collectionView) { collectionView, indexPath, asset in
return collectionView.dequeueConfiguredReusableCell(using: self.createCellRegistration(), for: indexPath, item: asset)
}

viewModel.fetchAssets()

viewModel.$assets
.sink { [weak self] assets in
self?.applySnapshot(assets: assets)
}
.store(in: &viewModel.cancellables)

// Handle errors
viewModel.$error
.sink { error in
if let error = error {
print("Error fetching assets: \(error)")
}
}
.store(in: &viewModel.cancellables)
}

private func createLayout() -> UICollectionViewLayout {
let layout = UICollectionViewCompositionalLayout { _, _ in
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .estimated(50))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let group = NSCollectionLayoutGroup.horizontal(layoutSize: itemSize, subitem: item, count: 1)
let section = NSCollectionLayoutSection(group: group)
return section
}
return layout
}

private func createCellRegistration() -> UICollectionView.CellRegistration<UICollectionViewListCell, Asset> {
return UICollectionView.CellRegistration<UICollectionViewListCell, Asset> { cell, indexPath, asset in
var contentConfiguration = UIListContentConfiguration.valueCell()
contentConfiguration.text = asset.name
contentConfiguration.secondaryText = "\(asset.symbol) - \(asset.priceUsd)"
cell.contentConfiguration = contentConfiguration
}
}

private func applySnapshot(assets: [Asset]) {
var snapshot = NSDiffableDataSourceSnapshot<Section, Asset>()
snapshot.appendSections([.main])
snapshot.appendItems(assets)
dataSource.apply(snapshot, animatingDifferences: true)
}
}

extension AssetListViewController: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
guard let asset = dataSource.itemIdentifier(for: indexPath) else { return }

//On tapping cell data is loaded in AssetDetailViewController
guard let asset = dataSource.itemIdentifier(for: indexPath) else { return }
coordinator?.showAssetDetail(asset: asset)
}
}

This is our view controller to present the data.



class APIUtility {
static let shared = APIUtility()

private init() {}

func fetchAssets() -> AnyPublisher<AssetData, Error> {
guard let url = URL(string: "http://api.coincap.io/v2/assets") else {
return Fail(error: NSError(domain: "Invalid URL", code: 0, userInfo: nil)).eraseToAnyPublisher()
}

return URLSession.shared.dataTaskPublisher(for: url)
.map { $0.data }
.decode(type: AssetData.self, decoder: JSONDecoder())
.eraseToAnyPublisher()
}
}


class AssetListViewModel: ObservableObject {
@Published var assets: [Asset] = []
@Published var error: Error?
private var cancellables = Set<AnyCancellable>()

func fetchAssets() {
APIUtility.shared.fetchAssets()
.receive(on: DispatchQueue.main)
.sink { completion in
if case let .failure(error) = completion {
self.error = error
}
} receiveValue: { assetData in
self.assets = assetData.data
}
.store(in: &cancellables)
}
}
protocol Coordinator {
func start()
func showAssetDetail(asset: Asset)
// Add other navigation methods as needed
}

class MainCoordinator: Coordinator {
private let navigationController: UINavigationController

init(navigationController: UINavigationController) {
self.navigationController = navigationController
}

func start() {
let assetListViewController = AssetListViewController.instantiate()
assetListViewController.coordinator = self
navigationController.pushViewController(assetListViewController, animated: false)
}

func showAssetDetail(asset: Asset) {
let assetDetailViewController = AssetDetailViewController.instantiate()
assetDetailViewController.asset = asset
navigationController.pushViewController(assetDetailViewController, animated: true)
}
}

extension UIViewController {
static func instantiate() -> Self {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
return storyboard.instantiateViewController(withIdentifier: String(describing: self)) as! Self
}
}

A little bonus, let’s inject the asset using a property wrapper:

@propertyWrapper
struct InjectAsset {
var asset: Asset

init(wrappedValue: Asset) {
self.asset = wrappedValue
}

var wrappedValue: Asset {
get { return asset }
set { asset = newValue }
}
}

class AssetDetailViewController: UIViewController {
// Use the property wrapper to inject the asset
@InjectAsset var asset: Asset

override func viewDidLoad() {
super.viewDidLoad()
print("Asset name: \(asset.name)")
}
}

How Clean Architecture is different from MVVM-C?

Clean Architecture and MVVM-C (Model-View-ViewModel-Coordinator) are both architectural patterns used in iOS development, but they serve different purposes and have different focuses. Here’s how they compare:

MVVM-C (Model-View-ViewModel-Coordinator):

1. Model: Represents the data model of the application.
2. View: Represents the user interface (UI) of the application.
3. ViewModel: Acts as an intermediary between the View and the Model. It exposes data from the Model to the View in a format suitable for presentation and translates user interactions from the View into actions that affect the Model.
4. Coordinator: Handles navigation and flow between different screens (ViewControllers) in the application. It typically abstracts away the navigation logic from the ViewControllers.

Characteristics:
1. Separation of Concerns: MVVM-C separates concerns by assigning specific responsibilities to each component. Views focus on UI rendering, ViewModels handle business logic, and Coordinators manage navigation flow.
2. Testability: MVVM-C promotes testability by isolating business logic from UI code, making it easier to write unit tests for ViewModels and Coordinators.
3. Reusability: Components in MVVM-C, especially ViewModels, can be reused across different parts of the application, promoting code reuse and maintainability.

Clean Architecture:

Clean Architecture, as proposed by Robert C. Martin (Uncle Bob), emphasises a clear separation of concerns and independence of the application’s business logic from external concerns like UI frameworks and database frameworks. It consists of multiple layers, including:

1. Entities: Represents the core business logic and data structures of the application.
2. Use Cases: Contains application-specific business rules and logic. It defines actions that can be performed within the application.
3. Interface Adapters: Converts data from the Use Cases into a format suitable for presentation in the UI and vice versa. This layer includes Presenters (or ViewModels), which are responsible for preparing data for display.
4. Frameworks & Drivers: Contains external frameworks, such as UI frameworks (e.g., UIKit) and database frameworks. These frameworks are considered implementation details and are kept separate from the core business logic.

Characteristics:

1. Decoupling: Clean Architecture promotes loose coupling between different components of the application, making it easier to modify and maintain the codebase.
2. Dependency Inversion Principle (DIP): Components in Clean Architecture depend on abstractions rather than concrete implementations, allowing for easier substitution and testing.
3. Isolation of Concerns: Each layer in Clean Architecture has a specific responsibility, and dependencies flow inward toward the core business logic, enforcing a clear separation of concerns.

Differences:

1. Focus: MVVM-C focuses more on structuring UI-related code and managing navigation flow, while Clean Architecture focuses on designing the core business logic of the application.
2. Layered Architecture vs. Pattern: Clean Architecture is an architectural design principle that can be applied to various design patterns, including MVVM-C. MVVM-C, on the other hand, is a specific pattern that addresses UI architecture and navigation flow.
3. Granularity: Clean Architecture provides a more granular separation of concerns with its layered approach, whereas MVVM-C combines ViewModel and Coordinator responsibilities into one layer for managing UI logic and navigation.

In summary, while MVVM-C and Clean Architecture share some similarities in promoting separation of concerns and testability, they serve different purposes and can be complementary in structuring iOS applications. Developers can choose the one that best fits the requirements and complexity of their projects.

Now let’s jump into converting our project from MVVM-C to Clean Architecture.

To comply with the Clean Architecture principles, this code will be split into three sections: Presentation, Domain and Data. Each of these segments will have its own duties ensuring that the system is better organized and easier to test. Here is a step by step guide on how to refactor AssetListViewController and AssetDetailViewController so that they work in this structure.

Presentation Layer:

  • Contains UI-related logic and components.
  • UIViewController instances reside in this layer.
  • Handles user input and presents data to the user.

Domain Layer:

  • Contains business logic and use cases.
  • Defines entities and business rules.

Data Layer:

  • Handles data retrieval and persistence.
  • Communicates with external sources like APIs or databases.

Presentation Layer:

import UIKit

protocol AssetListViewProtocol: AnyObject {
func presentFetchedAssets(_ assets: [Asset])
func displayError(_ error: Error)
}

class AssetListViewController: UIViewController, AssetListViewProtocol {
var presenter: AssetListPresenterProtocol!

override func viewDidLoad() {
super.viewDidLoad()
presenter.viewDidLoad()
}

// Implement AssetListViewProtocol methods
func displayAssets(_ assets: [Asset]) {
// Update UI with assets
}

func displayError(_ error: Error) {
// Display error message
}
}

// MARK: - AssetListPresenter

protocol AssetListPresenterProtocol {
var view: AssetListViewProtocol? { get set }
func fetchAssets()
}

class AssetListPresenter: AssetListPresenterProtocol {
weak var view: AssetListViewProtocol?
let interactor: AssetListInteractorProtocol

init(interactor: AssetListInteractorProtocol) {
self.interactor = interactor
}

func fetchAssets() {
interactor.fetchAssets()
}

func presentFetchedAssets(_ assets: [Asset]) {
view?.displayAssets(assets)
}

func presentError(_ error: Error) {
view?.displayError(error)
}
}

// MARK: - AssetListInteractor

protocol AssetListInteractorProtocol {
var presenter: AssetListPresenterProtocol? { get set }
func fetchAssets()
}

class AssetListInteractor: AssetListInteractorProtocol {
weak var presenter: AssetListPresenterProtocol?
let assetService: AssetServiceProtocol

private var cancellables = Set<AnyCancellable>()

init(assetService: AssetServiceProtocol) {
self.assetService = assetService
}

func fetchAssets() {
assetService.fetchAssets()
.sink(receiveCompletion: { [weak self] completion in
if case let .failure(error) = completion {
self?.presenter?.presentError(error)
}
}, receiveValue: { [weak self] assets in
self?.presenter?.presentFetchedAssets(assets)
})
.store(in: &cancellables)
}
}

protocol AssetServiceProtocol {
func fetchAssets() -> AnyPublisher<[Asset], Error>
}

struct AssetDataResponse: Codable {
let data: [Asset]
}

Domain Layer:

struct Asset {
let id: String
let name: String
let symbol: String
let priceUsd: String
}

Data Layer:

class AssetService: AssetServiceProtocol {
func fetchAssets() -> AnyPublisher<[Asset], Error> {
guard let url = URL(string: "http://api.coincap.io/v2/assets") else {
return Fail(error: URLError(.badURL)).eraseToAnyPublisher()
}

return URLSession.shared.dataTaskPublisher(for: url)
.map(\.data)
.decode(type: AssetDataResponse.self, decoder: JSONDecoder())
.map(\.data)
.eraseToAnyPublisher()
}
}

Conclusion:

Both MVVM-C and Clean Architecture are valid architectural patterns for iOS development, and the choice between them depends on factors such as project requirements, team expertise, and personal preferences. It’s essential to evaluate the specific needs and goals of your project and consider factors such as complexity, maintainability, testability, and scalability when selecting an architecture. Ultimately, the “better” architecture is the one that best fits the needs of your project and team.

I will recommend to read below blogs/Book for more information.

References:
https://betterprogramming.pub/ios-clean-architecture-using-swiftui-combine-and-dependency-injection-for-dummies-2e44600f952b

https://www.hackingwithswift.com/articles/71/how-to-use-the-coordinator-pattern-in-ios-apps

https://www.oreilly.com/library/view/clean-architecture-a/9780134494272/

--

--