iOS: Are You Truly Adopting MVVM? Or Just a Badly Shaped MVP?

Pedro Alvarez
Stackademic
Published in
5 min readDec 5, 2023

--

Image from https://www.photowall.co.uk/sunny-alps-in-chamonix-sweden-wallpaper

I will start this story with a disrupting fact: Probably you are not implementing MVVM in your UIKit project, but rather a version of MVP(Model-View-Presenter). People usually think that any architecture which places a presentation layer that is bound to the View is in fact MVVM, but some cases may not be. In fact, implementing MVVM in iOS was almost impossible before Combine, you could only do that via RxSwift. I tell you that because the main intrinsic property of MVVM is its total reactivity. You can only get UI update notifications from ViewModel by using the Observer pattern. If you are fetching logic events via closures or delegates, I am afraid you are rather implementing an MVP. For better understanding, let's recap what are these two different architectures:

Model-View-Presenter(MVP)

MVP came as a good solution to the excessive coupling we find in MVC(Massive View Controller for the intimate rs) and its basic idea was to introduce a new layer called Presenter that is responsible for delegating logical event responses to the UI as well as receiving UI interaction events back. It's a 1:1 relationship since View can only communicate with its own Presenter and it can only delegate events to a single View(although your View may assume other formats due protocol oriented programming and dependency injections). As the logical part is now replaced into the Presenter, we can much better manage our scene, debug and apply unit tests:

MVP draft

In the MVP with UIKit, we may to implement this relationship in two ways: Closures or Delegation. Both ways work because the Presenter can access the View through a reference. Take a look at this implementation:

struct Item {
let name: String
}

class Service {
func fetchData(completion: @escaping ([Item]) -> Void) {
// Irrelevant implementation
}
}

protocol MainPresenterDelegate: AnyObject {
func didReceiveItems(_ items: [Item])
func startLoading(_ flag: Bool)
}

protocol MainPresenterProtocol {
func didLoad()
func didSelectItem(_ item: Item)
}

class MainViewController: UIViewController, MainPresenterDelegate {

private let presenter: MainPresenterProtocol

init(presenter: MainPresenterProtocol) {
self.presenter = presenter
super.init(nibName: nil, bundle: nil)
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

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

// Irrelevant implementation

func didReceiveItems(_ items: [Item]) {
// Irrelevant implementation
}

func startLoading(_ flag: Bool) {
// Irrelevant implementation
}
}

class MainPresenter: MainPresenterProtocol {

private let service: Service
weak var view: MainPresenterDelegate?

init(service: Service) {
self.service = service
}

func didLoad() {
service.fetchData { [weak self] in
self?.view?.didReceiveItems($0)
}
}

func didSelectItem(_ item: Item) {
// Irrelevant implementation
}
}

Note that the Presenter is aware of the View and accesses its reference to send any sort of event it triggers. Probably this is the way you implement your "MVVM". Before WWDC19, it was the only possible native way of organizing an architecture in UIKit, despite RxSwift.

It has its advantages:

  • Decouples logic from the UI into Presenter
  • Improved testability
  • Improved readability and opportunity to better structure a Coordinator
  • Ideal to be implemented by an UIKit scene

And some disadvantages:

  • A complex chain of closures and delegation conformances may turn your code into massive
  • Your Presenter, which is a logical layer, is still aware of the View, so it's not totally isolated according to SOLID principles.
  • If you are not careful enough, your Presenter may also become massive(create new classes and models to avoid that)
  • Impossible to be implemented in SwiftUI due its declarative nature

The true MVVM(Model-View-ViewModel)

Now let's talk about what is truly an MVVM pattern from its raw concept. I imagine you have heard about the Observer pattern. Basically it consists of an Observable entity that shall emit events and there are some Observers, which the Observable is not aware of, that keep listening to the Observable state and handle the outputs in the proper way. The core idea is that the Observable doesn't has any reference to its observers and all the data propagation is via binding. In other words, MVVM is only possible with Reactive Programming:

Implementing an MVVM pattern in any UIKit scene is only possible with RxSwift or Combine, where you can subscribe to publishers and observables without the need of referencing the View/Controller.

Of course it better fits SwiftUI due its declarative nature and you can update your scene by just changing some published value that is observed by your View, but let's see how it works in UIKit:


protocol MainViewModelProtocol {
var itemsPublisher: AnyPublisher<[Item], Never> { get }
var startLoadingPublisher: AnyPublisher<Bool, Never> { get }
func didLoad()
func didSelectItem(_ item: Item)
}

class MainViewController: UIViewController, MainPresenterDelegate {

private let viewModel: MainViewModelProtocol

init(viewModel: MainViewModelProtocol) {
self.viewModel = viewModel
super.init(nibName: nil, bundle: nil)
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

override func viewDidLoad() {
super.viewDidLoad()
viewModel.didLoad()

viewModel
.itemsPublisher
.sink {
// Irrelevant implementation
}

viewModel
.startLoadingPublisher
.sink {
// Irrelevant implementation
}
}
}

class MainViewModel: MainViewModelProtocol {

private let service: Service

private let itemsSubject: PassthroughSubject<[Item], Never> = .init()
private let startLoadingSubject: PassthroughSubject<Bool, Never> = .init()

var itemsPublisher: AnyPublisher<[Item], Never> {
itemsSubject.eraseToAnyPublisher()
}

var startLoadingPublisher: AnyPublisher<Bool, Never> {
startLoadingSubject.eraseToAnyPublisher()
}

init(service: Service) {
self.service = service
}

func didLoad() {
startLoadingSubject.send(true)
service.fetchData { [weak self] in
self?.startLoadingSubject.send(false)
self?.itemsSubject.send($0)
}
}
}

Notice we don't need any delegate implementation anymore and our ViewModel doesn't have any idea about what exactly is subscribing to its observables. We only have bindings to itemsPublisher and startLoadingPublisher publishers whose subscriptions trigger some UI updates. With Combine you can even customize the data flow with operators that define some logic in the ViewModel side, replacing result on the main thread for example

Advantages:

  • Proper implementation of Observer Design Pattern
  • Total separation between UI and business
  • Easy logic configuration with the power of Combine and operators.

Disadvantages:

  • Possibly a high learning curve to people who are not used to Reactive programming
  • We need to take care to follow the correct chain of events in Combine publishers
  • We still need to configure observer chains in the View side(which is not needed in SwiftUI due ObservableObject`s)

Conclusion

This article may have been a great shock for people who swore in the past their company guild chapters have been implementing MVVM with UIKit this whole time. In fact, it's just a more confusing version of MVP with too many complex delegate implementations or closures. The core MVVM idea is to link both UI and business layers with the use of the Observer design pattern. The same works in Android with LiveData types, which work like publishers in Kotlin, but it's a topic for another article. I hope you now have a much deeper understanding about mobile architectures;).

Stackademic

Thank you for reading until the end. Before you go:

  • Please consider clapping and following the writer! 👏
  • Follow us on Twitter(X), LinkedIn, and YouTube.
  • Visit Stackademic.com to find out more about how we are democratizing free programming education around the world.

--

--

iOS | Android Developer - WWDC19 scholarship winner- Blockchain enthusiast