Why VIPER and MVVM in SwiftUI are actually the same pattern: A lesson in architectural thinking

18 min read Original article ↗

Architectural design patterns like VIPER might seem radically different from common ones like MVVM.

However, upon deeper inspection, it turns out that these patterns share the same constitutive components.

In this article, we will compare the MVVM and VIPER design patterns in SwiftUI and show how they follow the same principles.

FREE GUIDE - SwiftUI App Architecture: Design Patterns and Best Practices

MVC, MVVM, and MV in SwiftUI, plus practical lessons on code encapsulation and data flow.

DOWNLOAD THE FREE GUIDE

Table of contents

  1. Architectural design patterns share the same components, even when they seem widely different
  2. VIPER is a transposition for iOS development of the Clean Architecture
  3. The parallels between MVVM and VIPER stem from the roles of their components, not their names
  4. The views and entity layers of MVVM and VIPER are equivalent
  5. VIPER’s data stores are equivalent to MVVM’s controllers and other SwiftUI environment objects
  6. VIPER’s interactors implement an app screen’s business logic like MVVM’s view models
  7. VIPER’s presenters naturally occur in MVVM as root views
  8. VIPER handles navigation through separate wireframes, while MVVM uses a single coordinator
  9. VIPER’s dependencies must be created and injected at launch time

Architectural design patterns share the same components, even when they seem widely different

When it comes to adopting an architectural pattern for SwiftUI, a common yet unhelpful answer is that “it depends” on your app’s needs.

After many years of research, teaching, and practical implementations, I disagree with that answer.

While implementation details often differ, there are specific architectural components that any software application requires.

Software architecture is similar to classical architecture.

While buildings from different countries and periods differ widely, they still share the same constitutive principles and components, i.e., solid foundations, a roof, doors, windows, and a structure with specific proportions.

The same applies to software design patterns.

For this article, I chose MVVM and VIPER to highlight their similarities. Since they superficially look completely different, they offer a perfect opportunity to exercise your architectural thinking and understand how the underlying principles come from the same original pattern: MVC.

Note

I chose VIPER for this article purely for didactic reasons, not because it is particularly relevant to SwiftUI development nor because I recommend it.

VIPER didn’t gain traction in SwiftUI because, in my opinion, the initial attempts didn’t fully grasp the core architectural principles and thus failed to adapt the pattern to a new framework.

VIPER is a transposition for iOS development of the Clean Architecture

The main reason why developers think MVVM and VIPER are radically different is that, when you compare the diagrams you commonly find online, they look nothing like each other.

This misunderstanding is also exacerbated by other factors.

Nominally, VIPER is an iOS implementation of the Clean Architecture of Robert C. Martin, the same author of the SOLID principles. However, in my research, I never found an explicit explanation of how VIPER follows those architectural principles.

What you usually find are unhelpful links to this blog post, which does not really explain anything. Equally unhelpful is the often-cited circular diagram that you can also find in this article.

This diagram alone isn’t necessarily wrong. It is actually useful to show the dependency rule outlined in the linked article.

The problem is the assumption that it clearly shows VIPER’s structure and principles, which it doesn’t.

Why VIPER and the Clean Architecture look so different from MVC and MVVM

What I never see is the actual architectural diagram of a typical scenario from the Clean Architecture book, which would at least better align with VIPER.

However, this diagram still hides the parallels between the Clean Architecture and MVC and MVVM as commonly understood in iOS development.

If anything, it makes the matter even muddier since it introduces several novel terms, such as interactor, presenter, or entities.

Moreover, it makes matters worse by using known terms in ways we are not accustomed to in iOS and SwiftUI development, i.e., the controller and view model components you see in the diagram are not what you would usually identify as such in SwiftUI.

FREE GUIDE - SwiftUI App Architecture: Design Patterns and Best Practices

MVC, MVVM, and MV in SwiftUI, plus practical lessons on code encapsulation and data flow.

DOWNLOAD THE FREE GUIDE

The parallels between MVVM and VIPER stem from the roles of their components, not their names

As the common adage goes, a picture is worth a thousand words. While the wrong diagram can mislead you, the correct one can immediately reveal how MVVM and VIPER are practically identical.

The trick is recognizing the role each component plays in the app’s overall architecture, rather than merely considering what they are called.

It is also important to note that the correspondence between elements is not strictly one-to-one; it is only in roles.

As we will see, VIPER prescribes a wireframe for each module, which is externally configured by a central dependency object. MVVM, instead, generally uses a single coordinator that incorporates routing and dependency injection.

Best practice

You should adopt design patterns and architectural principles only when they benefit you.

VIPER is extremely prescriptive, forcing unnecessary abstractions and filling your code with boilerplate. It causes a high degree of indirection that is tedious to implement and hard to follow, with no particular benefit over more flexible approaches.

For these reasons, I do not recommend using VIPER in your SwiftUI apps, even though, as I will show, it follows the same ideas as MVVM.

To show the parallels between VIPER and MVVM, I will recreate the Todo app presented in the original article that popularized VIPER.

Since that app was built in UIKit, the resulting SwiftUI app will be slightly different to adapt to the framework.

Moreover, I will omit some superfluous elements that the original app implemented to strictly adhere to the full Clean Architecture diagram, since they do not add anything to our discussion, and I am not a fan of strict patterns anyway.

You can find the final Xcode project on GitHub.

The views and entity layers of MVVM and VIPER are equivalent

The most straightforward parallel we can draw is between the view layers of both patterns.

Both in MVVM and VIPER, views are independent of the underlying implementation. These are what I call content views, which only receive simple types as parameters and are immediately previewable in Xcode.

struct AddContentView: View {
	@Binding var name: String
	@Binding var date: Date
	let saveAction: () -> Void
	let cancelAction: () -> Void

	var body: some View {
		Form {
			TextField("Name", text: $name)
			DatePicker("Date", selection: $date)
				.datePickerStyle(.graphical)
		}
		.navigationTitle("Add todo")
		.toolbar {
			ToolbarItem {
				Button("Save", role: .confirm, action: saveAction)
			}
			ToolbarItem(placement: .cancellationAction) {
				Button("Cancel", role: .cancel, action: cancelAction)
			}
		}
	}
}

#Preview {
	@Previewable @State var name: String = ""
	@Previewable @State var date: Date = Date()
	NavigationStack {
		AddContentView(
			name: $name,
			date: $date,
			saveAction: {},
			cancelAction: {})
	}
}

The other straightforward parallel is between MVVM’s_ model types_ and VIPER’s_ entities_, which represent the app’s data types.

We will use SwiftData in our app, so our only data type is a @Model class.

@Model
final class TodoItem {
	var name: String
	var date: Date

	init(name: String, date: Date) {
		self.name = name
		self.date = date
	}
}

Providing data formatting to views through a model transformation layer

One of the strict prescriptions of VIPER is to convert data coming from lower layers into a viewable form. That would be the view model component you can see in the original Clean Architecture diagram, which differs from the view model layer of MVVM.

This is an idea I use from time to time, which I call the view data layer instead. A view data type contains the logic to format data for display by the view layer.

However, I generally do not like creating a plethora of tiny types with little to no logic. I would use a view data type only when model types are difficult to instantiate.

Otherwise, a more straightforward, Swifty approach is to use Swift extensions for the formatting logic.

extension TodoItem {
	var weekday: String {
		let calendar = Calendar.current
		return calendar.weekdaySymbols[calendar
			.component(.weekday, from: date) - calendar.firstWeekday + 1]
	}
}

extension [TodoItem] {
	static let today: [TodoItem] = [
		TodoItem(name: "Grab coffee", date: Date())
	]

	static let nextWeek: [TodoItem] = [
		TodoItem(
			name: "Stock up on water",
			date: Calendar.current.date(byAdding: .day, value: 7, to: Date())!
		),
		TodoItem(
			name: "Visit labs",
			date: Calendar.current.date(byAdding: .day, value: 8, to: Date())!
		),
		TodoItem(
			name: "Fly home",
			date: Calendar.current.date(byAdding: .day, value: 9, to: Date())!
		)
	]
}

This allows using model types straight away in content views without needing intermediate types and the corresponding conversion code, and without losing the view’s previewability.

struct ListContentView: View {
	let todayItems: [TodoItem]
	let nextWeekItems: [TodoItem]
	let addAction: () -> Void

	var body: some View {
		List {
			Section("Today") {
				ForEach(todayItems) { item in
					Label(item.name, systemImage: "checkmark")
				}
			}
			Section("Next week") {
				ForEach(nextWeekItems) { item in
					Label(item.name, systemImage: "calendar")
						.badge(Text(item.weekday))
				}
			}
		}
		.navigationTitle("Todo")
		.toolbar {
			Button("", systemImage: "plus", action: addAction)
		}
	}
}

#Preview {
	NavigationStack {
		ListContentView(
			todayItems: .today,
			nextWeekItems: .nextWeek,
			addAction: {
			})
	}
}

FREE GUIDE - SwiftUI App Architecture: Design Patterns and Best Practices

MVC, MVVM, and MV in SwiftUI, plus practical lessons on code encapsulation and data flow.

DOWNLOAD THE FREE GUIDE

VIPER’s data stores are equivalent to MVVM’s controllers and other SwiftUI environment objects

When we concentrate on the app’s business logic, it’s also pretty straightforward to draw parallels between MVVM and VIPER.

VIPER’s data stores are (shared) objects that access an underlying centralized data store, such as a database or a REST API.

These are what you would call controllers in MVC and MVVM, but you might also refer to them as managers, services, or other names.

A data store object provides an agnostic interface to the underlying database, decoupling it from the rest of the app. Since we are using SwiftData, our data store provides methods to fetch and save data in a SwiftData model context.

@Observable class DataStore {
	let modelContext: ModelContext

	init(modelContext: ModelContext) {
		self.modelContext = modelContext
	}

	func todoItems(between start: Date, and end: Date) -> [TodoItem] {
		let items = try? modelContext.fetch(
			FetchDescriptor(
				predicate: #Predicate { $0.date > start && $0.date < end },
				sortBy: [SortDescriptor(\.date)]
			)
		)
		return items ?? []
	}

	func save(_ item: TodoItem) {
		modelContext.insert(item)
		try! modelContext.save()
	}
}

Clean architecture and VIPER prescribe transforming the underlying store data into our app’s entities, which should remain separate. You can see how that can be a useful concept when the underlying database returns data in a raw format, such as database rows, or the JSON data returned by a REST API.

However, in our case, the SwiftData model context already uses the same TodoItem class we use for our app’s entities, so there is no need for two separate data representations.

VIPER’s interactors implement an app screen’s business logic like MVVM’s view models

A less obvious parallel to draw is between VIPER’s interactors and MVVM’s view models. However, this is only because of their names, and because Clean Architecture uses the term view model for a different component.

If instead we look at their architectural roles, the connection is pretty evident. Interactors, like view models, implement the app’s business logic for a specific app screen.

So, for example, we can create an interactor that fetches the todo items for today and next week from our SwiftData data store.

@Observable class ListInteractor {
	let dataStore: DataStore
	private(set) var todayItems: [TodoItem] = []
	private(set) var nextWeekItems: [TodoItem] = []

	init(dataStore: DataStore) {
		self.dataStore = dataStore
	}

	func findUpcomingItems() {
		let calendar = Calendar.current
		let today = calendar.startOfDay(for: Date())
		let tomorrow = calendar.startOfDay(
			for: calendar.date(byAdding: .day, value: 1, to: today)!
		)
		todayItems = dataStore.todoItems(between: today, and: tomorrow)
		let nextWeek = calendar.date(byAdding: .day, value: 7, to: today)!
		let endOfNextWeek = calendar.nextWeekend(startingAfter: nextWeek)!.end
		nextWeekItems = dataStore.todoItems(between: nextWeek, and: endOfNextWeek)
	}
}

One way in which VIPER differs from MVVM is that an interactor should not let entities pass through the output boundary; instead, it should return a dedicated output data structure.

This would create another plain Swift type, which is rarely beneficial in a SwiftUI app. Our ListInteractor returns arrays of TodoItem, which we already let emerge at the view level into the ListContentView.

Another difference between VIPER and my recommended MVVM approach is that VIPER prescribes an interactor for every app module, whereas I would implement a view model for an app’s screen only when required.

For example, the interactor for adding a new todo item contains very little code. I would not bother creating such a tiny view model; I would leave the code in the view instead.

@Observable class AddInteractor {
	let dataStore: DataStore
	var name: String = ""
	var date: Date = Date()

	init(dataStore: DataStore) {
		self.dataStore = dataStore
	}

	func save() {
		let newItem = TodoItem(name: name, date: date)
		dataStore.save(newItem)
	}
}

FREE GUIDE - SwiftUI App Architecture: Design Patterns and Best Practices

MVC, MVVM, and MV in SwiftUI, plus practical lessons on code encapsulation and data flow.

DOWNLOAD THE FREE GUIDE

VIPER’s presenters naturally occur in MVVM as root views

Probably, the least evident parallel is how to translate VIPER’s presenters into MVVM in SwiftUI.

Looking at the various attempts I found online, there is a recurring effort to force an extra presenter object into the pattern, which does not work well with SwiftUI.

The solution is in realizing that not all SwiftUI views are the same.

Most of an app’s views are content views, including all the standard SwiftUI views provided by the framework. The views at the root of a hierarchy, however, are different.

These are the views that usually access shared objects (controllers/data stores) or state objects (view models/interactors). Thus, VIPER’s presenters are, in SwiftUI, what I call root views.

Root views act as a bridge between content views and lower-level architectural layers, interpreting the user’s input and actions in accordance with the app’s business logic.

struct ListPresenterView: View {
	@State var interactor: ListInteractor

	var body: some View {
		ListContentView(
			todayItems: interactor.todayItems,
			nextWeekItems: interactor.nextWeekItems,
			addAction: { }
		)
		.onAppear { interactor.findUpcomingItems() }
	}
}

Again, the prescriptive nature of VIPER requires a presenter for every app module, which is no different from MVVM, which naturally leads to a root view for almost every app screen.

struct AddPresenterView: View {
	@State var interactor: AddInteractor

	var body: some View {
		NavigationStack {
			AddContentView(
				name: $interactor.name,
				date: $interactor.date,
				saveAction: { interactor.save() },
				cancelAction: { }
			)
		}
	}
}

VIPER handles navigation through separate wireframes, while MVVM uses a single coordinator

For now, each module exists in isolation, without referencing the others. This is intentional, as Clean Architecture and VIPER aim to minimize coupling.

In VIPER, the routing responsibility, i.e., navigation, is shared across the presenter and the wireframe layers. Loosely speaking, this is akin to using coordinators in MVVM.

Here, the correspondence is not one-to-one. VIPER requires each module to have its own wireframe that creates the module’s presenter and connects it to other modules.

@Observable class AddWireframe {
	let interactor: AddInteractor
	var delegate: AddDelegate?

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

	var interface: some View {
		NavigationStack {
			AddPresenterView(interactor: interactor, wireframe: self)
		}
	}

	func finish() {
		delegate?.addModuleDidFinish()
	}
}

protocol AddDelegate {
	func addModuleDidFinish()
}
struct AddPresenterView: View {
	@State var interactor: AddInteractor
	@State var wireframe: AddWireframe

	var body: some View {
		AddContentView(
			name: $interactor.name,
			date: $interactor.date,
			saveAction: {
				interactor.save()
				wireframe.finish()
			},
			cancelAction: { wireframe.finish() }
		)
	}
}

As wireframes control the app’s structure and navigation, they inject the dependencies into the presenters, including themselves.

Since the add module is presented by some other module, the AddWireframe needs to use delegation to notify the presenting wireframe to navigate back. In MVVM, this would be simpler, as the AddPresenterView would communicate directly with the navigation coordinator.

The list module gets its own wireframe, which defines its interface and its connection to the add module via AddWireframe.

@Observable class ListWireframe {
	let listInteractor: ListInteractor
	let addWireframe: AddWireframe
	private(set) var isAddInterfacePresented = false

	init(listInteractor: ListInteractor, addWireframe: AddWireframe) {
		self.listInteractor = listInteractor
		self.addWireframe = addWireframe
	}

	var interface: some View {
		@Bindable var wireframe = self
		return NavigationStack {
			ListPresenterView(interactor: listInteractor, wireframe: self)
				.fullScreenCover(isPresented: $wireframe.isAddInterfacePresented) {
					self.addWireframe.interface
				}
		}
	}

	func presentAddInterface() {
		isAddInterfacePresented = true
	}
}

extension ListWireframe: AddDelegate {
	func addModuleDidFinish() {
		isAddInterfacePresented = false
	}
}
struct ListPresenterView: View {
	@State var interactor: ListInteractor
	@State var wireframe: ListWireframe

	var body: some View {
		ListContentView(
			todayItems: interactor.todayItems,
			nextWeekItems: interactor.nextWeekItems,
			addAction: { wireframe.presentAddInterface() }
		)
		.onAppear { interactor.findUpcomingItems() }
		.onChange(of: wireframe.isAddInterfacePresented) {
			interactor.findUpcomingItems()
		}
	}
}

The disadvantages of strict patterns like VIPER, VIP, Clean Swift, and TCA

While VIPER’s wireframes guarantee complete isolation between modules, this comes at the cost of a far more complex architecture and additional boilerplate code to connect modules through their wireframes.

It also ignores the affordances offered by SwiftUI, such as environment objects and SwiftData’s @Query macro, because responsibilities must be strictly assigned to each component.

This is by design, and it is what makes VIPER unnecessarily convoluted and hard to follow unless you are already familiar with its structure.

This happens with any other SwiftUI pattern that follows the strict separation dictated by the Clean Architecture, like Clean Swift, which its author seems to have abandoned.

This is also evident in The Composable Architecture, as testified by this experience. The whole pattern relies on a proprietary framework that abandons SwiftUI’s core features in favor of a plethora of custom macros.

Like VIPER’s wireframes, TCA moves the entire app structure away from SwiftUI views into connected reducers. This is exacerbated by an extensive use of Swift enumerations to represent state and its transitions, which I consider an antipattern.

FREE GUIDE - SwiftUI App Architecture: Design Patterns and Best Practices

MVC, MVVM, and MV in SwiftUI, plus practical lessons on code encapsulation and data flow.

DOWNLOAD THE FREE GUIDE

VIPER’s dependencies must be created and injected at launch time

There is one last piece to the puzzle. VIPER’s wireframes only define the connections between modules. However, wireframes are not responsible for instantiation or dependency injection.

That is usually done through a central object that is never shown in any architectural diagram. In VIPER, this is called the dependencies object, and it is again similar to an MVVM coordinator.

@Observable class Dependencies {
	let modelContainer: ModelContainer
	let dataStore: DataStore
	let rootWireframe: ListWireframe

	init() {
		let schema = Schema([TodoItem.self])
		let configuration = ModelConfiguration(
			schema: schema,
			isStoredInMemoryOnly: false
		)
		let container = try! ModelContainer(
			for: schema,
			configurations: [configuration]
		)
		modelContainer = container
		dataStore = DataStore(modelContext: container.mainContext)
		rootWireframe = Self.setDependencies(dataStore: dataStore)
	}

	var rootInterface: some View {
		rootWireframe.interface
	}

	static func setDependencies(dataStore: DataStore) -> ListWireframe {
		let listInteractor = ListInteractor(dataStore: dataStore)
		let addInteractor = AddInteractor(dataStore: dataStore)
		let addWireframe = AddWireframe(interactor: addInteractor)
		let listWireframe = ListWireframe(
			listInteractor: listInteractor,
			addWireframe: addWireframe
		)
		addWireframe.delegate = listWireframe
		return listWireframe
	}
}
@main
struct TodoApp: App {
	@State private var dependencies = Dependencies()

	var body: some Scene {
		WindowGroup {
			dependencies.rootInterface
		}
	}
}

However, there is also a fundamental difference. An MVVM coordinator creates and injects dependencies only as needed. The Dependencies class above, instead, creates the entire dependency graph at launch.

This can cause several problems.

First of all, it unnecessarily increases your app’s memory footprint.

Moreover, the dependency object graph retains and reuses wireframe and interactor objects. Using common @State properties instead creates new instances as the user navigates through the app.

If you run the Todo app, you will see this problem when the Add Todo app screen is presented a second time. After creating one todo item, adding a second one will display a user interface that shows old data rather than an empty one.

I intentionally didn’t fix that to show you the problems arising from an unnecessarily complicated architecture that goes against how SwiftUI works.

This can obviously be fixed in multiple ways, but it adds extra cognitive load on the developer, who needs to fix or prevent issues that wouldn’t occur in vanilla SwiftUI code.

Conclusions

While MVVM and VIPER may look radically different at first glance, it is possible to draw parallels between their components based on their roles.

Views and entities work the same in both patterns, data stores are the same as controllers, and interactors are equivalent to view models.

The parallel between other layers might seem less straightforward, but presenters can be compared to the root views that naturally emerge in SwiftUI apps, and wireframes cover the same role as coordinators.

Despite all these parallels, VIPER remains more prescriptive and convoluted than MVVM. Its architecture is harder to follow, leading to a lot of superfluous boilerplate code.

Finally, its complexity can cause structural issues and imposes a higher cognitive load on the developer.

FREE GUIDE - SwiftUI App Architecture: Design Patterns and Best Practices

MVC, MVVM, and MV in SwiftUI, plus practical lessons on code encapsulation and data flow.

DOWNLOAD THE FREE GUIDE