A complete failure of MVVM+C in SwiftUI

Learn value type and refactor

Jim Lai
10 min readMay 18

--

John Wick helping a fellow MVVM dev

You working again, John?

Afraid so.

Saw this on highlight.

How to build UIKit like MVVM-C Coordinator hierarchy with SwiftUI

As usual, I’m going to rant about it. At the very least this should give you a perspective outside of MVVM echo chamber.

Let’s appreciate this iconic MVVM+C design:

final class UserFlowCoordinator: ObservableObject, Hashable {
// ...
private func usersListView() -> some View { // create a view
let viewModel = UsersListViewModel()
let usersListView = UsersListView(viewModel: viewModel)
bind(view: usersListView)
return usersListView
}
// ...
}

This is supposed to be more scalable than this?

struct UserList: View {...}
UserList(...) // create a view

Really?

Oh, reusable, separate logic from view… etc. Of course.

But is it? Is it really reusable? Do you actually separate logic from view?

You can’t reuse UserListViewModel , most likely you will need to inherit it.

Is this separate logic from view?

struct UserList: View {
@ObservedObject var sink: SinkObject
// dump everything to sink
}

View is still associated with logic. You just now access it via a wrapper.

Logic is not simplified, code is not reduced. More reusable? But you dump everything to it, which reduces it; You need to create a new class anyway to inherit it, which reduces it. In the meantime you ignore other means of refactor like computed properties and protocol extension.

You will argue that it’s not a fair comparison, because I ignored DI and coordinator logic. And you are right.

But, do you really need it? Let’s see his argument.

Oh, he has no argument

You need it because it worked so well in UIKit.

But it is a dog shit even in UIKit. I’m sorry, let’s approach this in a more objective way.

He started with MVVM being an well-established design pattern. Then coordinator is added to separate routing logic from view.

This is objectively false. In UIKit you separate routing logic from ViewController . You can only access NavigationViewController from there. Even he knew this:

The whole idea behind introducing MVVM-C over MVVM to separate app’s routing logic from the view layer. In UIKit we can do it perfectly since we can scratch the UINavigationController out of the ViewController and use it separately.

So objectively, he didn’t mention anything as to why he thought it’s a good idea to port something built for UIKit to SwiftUI, despite of them having completely different architecture; he didn’t show any argument or comparison as to why this is better than using basic SDK. And he didn’t even get basic fact right.

Not only did he think you can just port UIKit coordinator-like hierarchy to SwiftUI, but also it would be an improvement over a single coordinator approach.

Most famous approach to use Coordinator pattern with SwiftUI is to use single coordinator throughout the whole application as an observable object. Here, we are only using coordinator for navigation purpose and we aren’t using coordinator to handle other responsibilities which we used to do in UIKit MVVM-C Coordinator. This approach has following drawbacks.

Wait, you used coordinator to handle other responsibilities? So it’s not just a coordinator then?

Let’s take a moment to appreciate how full of shit MVVM+C is.

And there are drawbacks! That’s not what MVVM+C tutorial said!

Oh now it suddenly occured to you that MVVM+C has drawbacks!?

What other shit did these MVVM devs not disclose before?

Single coordinator responsible for handling all the navigations.

Coordinator becomes too heavy when the app is scaling.

Coordinator knows too much and hard to modularise the app.

Becomes less readable & hard to maintain when the app grows.

You can already see a pattern here: these guys don’t know a thing about REFACTOR.

All they know is create some God object. Dump everything to it until it becomes too crowded. Then you create MORE GOD OBJECTS!

So for EVERY view, we at mimimum need to have 1 view model, 1 coordinator, 1 function to package all that shit to the view so they are SEPARATED. And to reuse any of them, you have to create new classes / protocols and inherit old classes.

What a load of shit. I’m sorry, let’s look at this from a more objective perspective.

There’s nothing that says you can only create one coordinator in the first place. So he is not wrong yet. But nothing says you can only create one observable in view either. His argument can apply to view model as well.

So why didn’t he do the same for view model? Obvisouly MVVM would collapse then. E.g.;

struct ContentView: View {
@StateObject private var appCoordinator = AppCoordinator(path: NavigationPath())
// ...
}

Where do you put view model then? By MVVM’s definition, your “view”contains “logic” now.

This is a singularity that MVVM cannot explain. All they can do is avoid it, like what he has to do with @EnvironmentObject.

        NavigationStack(path: $appCoordinator.path) {
// ...
}
.environmentObject(appCoordinator)

Nowhere in his code snippets did he observe environment object. So what’s the point of having it as an environment object?

He can’t observe it, (at least explicitly), otherwise “view” would contain “logic”, would it?

This lack of respect for SDK is precisely why MVVM is dog shit. It doesn’t matter whether it is UIKit or SwiftUI. There are always excuses for MVVM devs to ignore SDK so they can write design pattern for the sake of writing design pattern.

I’m sorry, let’s keep things objective and take a more in-depth look at coordinator(s).

Reference type + manual binding versus value type + automatic binding

Given the choice, would you prefer NOT to manually create a view model followed by manually creating binding? Because these are routine boilerplate?

No. How else can I virtue-signal my prowess as a lead iOS dev that pioneered MVVM+C in SwiftUI?

I want to repeat the word “view model” as many times as I could. People need to be constantly reminded that I’m writing MVVM. E.g.;

private func bind(userCoordinator: UserFlowCoordinator) {
userCoordinator.pushCoordinator
.receive(on: DispatchQueue.main)
.sink(receiveValue: { [weak self] coordinator in
self?.push(coordinator)
})
.store(in: &cancellables)
}

This right here is material for 3 tutorials covering MVVM + Combine + C, in which I can talk about how SwiftUI is designed so you can do manual binding easily.

What I wouldn’t cover is that why you need it? E.g.;

enum Pages {
case user1, user2, user3, setting1, setting2
}
// ...
@EnvironmentObject var coo: Coordinator
// ...
// coo.path = [user1, user2, setting1, setting2]
// ... NavigationStack($coo.path)

Automatic binding. Value type.

But this doesn’t cover multiple coordinators.

You can easily apply this to a subtree of view hierarchy. Then multiple subtrees. These coordinators can be observable properties of an enclosing coordinator, which is observable. E.g.;

struct UserList: View {
@EnvrionmentObject var coo: Coordinator
// ... NavigationStack($coo.path)
// let userFlowCoo worry about updating coo.path
// ... coo.userFlowCoo.changePage()
}

Automatic binding. Value type. Oh, and dependency injection since we don’t know anything about how changePage() is implemented.

Tell me again how MVVM+C is supposed to scalable?

There are a lots of benefits on this approach compared to single coordinator approach with SwiftUI. Those are,

Easier to scale and modularise the app by adding different flow coordinators.

You can further break down the flows by adding more sub coordinators.

I think there’s a misunderstanding here. That you can add more when needed doesn’t mean it can scale. The key to scale is how NOT to add anything or as few as possible.

Having to create one view model for every view is the complete opposite of scalable. Not only MVVM devs ignore that, they are adding more! And they see it as a feature not bug!

This concept has a name. It’s called REFACTOR.

Oh, not to mention. Did he really put reference types as navigation stack path? Let me check again.

Technical Lead (Swift, Objective C, Flutter, react-native) | iOS Developer | Mobile Development Lecturer

Oh sorry, copy paste the wrong thing.

    private func push<T: Hashable>(_ coordinator: T) {
path.append(coordinator)
}

Shit…

Not that you can’t. But wouldn’t it be simpler to just use value type? You don’t have to implement Hashable for EVERY coordinator for one?

    func hash(into hasher: inout Hasher) {
hasher.combine(id)
}

In case you didn’t notice, all he did so far is creating reference types. (besides view) In addition, he promised that more can be added and hence scalable… in a SDK where even “view” is a value type.

Navigation stack is now where you build view:

    private func usersListView() -> some View {
let viewModel = UsersListViewModel()
let usersListView = UsersListView(viewModel: viewModel)
bind(view: usersListView)
return usersListView
}

This defeats the purpose of having immutable value type as view and automatic binding.

If it were me writing this, I’ll stop and compare it to

struct UserList: View {...}
// UserList()

And realize I have no advantage, or at least not significant enough to justify the overheads.

On the other hand, I won’t be able to use this view directly:

struct MainSettingsView: View {
let didClickPrivacy = PassthroughSubject<Bool, Never>()
let didClickCustom = PassthroughSubject<Bool, Never>()

var body: some View {
// ... didClickPrivacy.send()
}
}

Because I don’t know how to setup these passthrough objects. This view is designed only to be generated during runtime by a coordinator. You can’t even get it from coordinator directly because this function is private.

    private func mainSettingsView() -> some View {
let mainView = MainSettingsView()
bind(view: mainView)
return mainView
}

So view is coupled with coordinator. I see how MVVM succeeded in separating logic from view.

This is important because you expect “view” to be created and destroyed on demand, especially when it is a value type. And you want to be able to preview or test it.

He didn’t bother using view model in this case because even he got tired of writing that shit. How many nested levels do you need for a view? What do you gain from NOT using environment object?

In basic SwiftUI flow, external dependencies are shared most likely via environment object. The rest should be locally created unless you are doing DI dumb shit. Create one and use it to preview or test.

Or do you rather want this?

    private func bind(view: MainSettingsView) {
view.didClickPrivacy
.receive(on: DispatchQueue.main)
.sink(receiveValue: { [weak self] didClick in
if didClick {
self?.showPrivacySettings()
}
})
.store(in: &cancellables)

view.didClickCustom
.receive(on: DispatchQueue.main)
.sink(receiveValue: { [weak self] didClick in
if didClick {
self?.showCustomSettings()
}
})
.store(in: &cancellables)
}

It’s private so can’t be called from outside. (a binding that is not reusable) Copy paste the same shit twice, for ONE view. What if you need to do it for multiple views? Oh that’s right, we can add more if needed, and that is scalable. If you know anything about REFACTOR, this is where you do it!

Somebody needs to stop dumb shit MVVM. I’m writing this so more people can come forward. MVVM is dog shit unless you have binding support. And it may still be dog shit given how these guys just ignored automatic binding.

Let’s wrap up.

Everything’s got a price.

In MVVM, creating objects and manual binding is free. The inevitable result is brute force boilerplate. When you get a free pass to create one view model for each view on top of other dumb shit, you don’t have any incentive to refactor.

No. Creating types has a price. Interactions (callback, delegate, binding) has a price. Passing objects has a price. Everything’s got a price.

For example, this is seen as a feature in MVVM+C

That is 7 files for a view. You repeat the same thing for EVERY view.

A junior level developer should be able to refactor and combine all these.

One Codable model type for each response? Refactor.

One view model for each view? Refactor.

One coordinator for each view? For God’s sake, who wrote this shit?

So you might start with using a JSON type for every response. Remove view model because everything can conform to observable and binding is automatic. Then try using no coordinator unless you really need one. Use value type for navigation stack path, not this full-fledged coordinator reference type that only does routing, binding, view generation in private and you should be grateful MVVM+C doesn’t do more than that. Not to mention the brute force binding code, unecessary DI, extra overheads that are somehow scalable.

Compare to his conclusions again:

Conclusion

There are a lots of benefits on this approach compared to single coordinator approach with SwiftUI. Those are,

Easier to scale and modularise the app by adding different flow coordinators.

You can further break down the flows by adding more sub coordinators.

Main coordinator doesn’t become too heavy to handle.

Each coordinator only knows about navigations of its own flow only. This will make easier to maintain the the navigations of the each flow.

Becomes more readable & easy to maintain when the app grows.

Most are just due to having more objects / interactions / overheads.

Why would a coordinator know about navigation other than its own flow in the first place? This is just basic design requirement.

Easy to maintain… compared to one file per view? I hope you at least feel plausible that I can put everything in one “view” and it will be more compact than your view + view model + coordinator.

Finally,

This approach is open to your thoughts as well. If you see anything to be improved or changed on this approach, just let me know via comments.
Let’s improve this together. Have a nice coding !

Well, since you asked. It’s wrong on so many levels that it needs an article review.

View model is not free, coordinator is not free, coordination and injection is not free. Everything’s got a price.

Consider this as a professional courtesy.

--

--

Jim Lai