Mastering the art of over-generalization
If I tell people that I write MVC, people think I write massive view controller.
But if I tell people I write clean architecture + MVVM + Coordinator…
The iOS developer road map in 2022 is as follows:
Day1: hello world
Day2: google MVVM
Day3: google clean architecture
Day4: google MVVM+C
Day5: publish tutorials on applying MVVM+C to SwiftUI, not because of refactor reasons, but just for the sake of writing design pattern
(Days spent on learning SwiftUI and refactor: 0)
This article SwiftUI Coordinators published under “better programming” demonstrates this process. It’s full of anti-patterns, nonsensical and redundant designs, over-generalization and boilerplate.
The only reason it can be published under “better programming” is because it has the word MVVC+C in it. Obviously this guy isn’t the only one. I’m using this one to showcase the idiotic Swift that is common in the industry.
What’s the refactor reason?
We desire to do the same with SwiftUI, but problem is that in this framework everything regarding UI is declarative and we deal with value types(structs). So we cannot have a reference that we can directly ask to change context
This implies MVVM+C has a reference that you can directly ask to change context. Isn’t this the kind of abuse we want to avoid by introducing a value-type based SDK? And now he desires to do the same.
This alone should raise alerts. The underlying tone should not be “going around this obstacle”, because it almost guarantees a shady solution in which you implicitly trigger side effects without view owner knowing it.
What you should do, if your intention is not pumping buzzwords, is to rephrase your requirement in the context of SwiftUI. E.g.;
- I need a centralized navigation control, which is stateful, which means it needs to be shared across some part of view hierarchy.
- Must be @ObservedObject, more likely an @EnvironmentObject.
- Design views that explicitly observes it.
Now think about what a shady solution might look like?
Views won’t know they are observing a coordinator. Then obviously there’s going to be a middle man. This middle man has to publish again whatever coordinator has to publish. If SwiftUI automatically does this for you, you risk implicit nested publisher if not redundant wrapper, if SwiftUI does not do this for you, you write boilerplate and nested publisher. On top of that, you have to justify it not violating single responsibility principle.
You should prefer explicit reference and flat structure. These are the refactor reasons you want to discuss first and foremost.
Obviously the author forego all these discussions. His focus is to build a MVVM+C in SwiftUI. All MVVM pumpers ignore refactor reasons. Because if you do, you will realize there’s no reason creating view model at all in SwiftUI. Building design patterns on view models that have no reasons to exist is like building a house on sand.
House built on sand
And everyone who hears these words of mine and does not do them will be like a foolish man who built his house on the sand. Matthew 7:24
This structure is for every view. This design makes no sense because it defeats the purpose of having a centralized coordinator. Oh shit, MVVM+C in UIKit has a coordinator per view controller isn’t it? God damn that is some dumb shit. Coordinator has be on the scope above views, so it can coordinate between views. In UIKit you can kind of get away with it because you can get
So instead of creating a shared coordinator, the dude injects
CoordinatorViewModel to every view. Wait, why is it a view model? This creates a coupling between coordinator and a view that doesn’t need to be there! Even in UIKit, coordinator doesn’t have its own view, because it doesn’t need to. It manages controllers which have their own view. The key word is manage, not view.
This highlights a common problem of idiotic Swift. You can’t have regular shared service and state machines anymore. You can only have view model which implies you need to have a view.
In this case, if you observe
@EnvironmentObject var coo: Coordinator
@ObservedObject var vm = ViewModel()
Then people will ask if you can put properties outside of view model, what’s the point of putting everything in view model? Especially you get to use @EnvironmentObject outside but not inside of an observable. Then the Ponzi collapses, because there is no point of putting everything in a sink object in the first place. View model does everything including networking, coordinating… except binding, the one thing it is supposed to do.
So somehow the dude turned a service design into view design. The
Coordinator is dead at this point. It is way easier to just navigate using SDK since a view has to handle its own navigation anyway. You might as well use specific type instead of type-erased
some View to gain benefits of static type checks.
Not to mention, you are now writing 2 view models per view!
Any dev with self-respect will stop at this point. Something must be wrong besides being a dog shit design.
But no, MVVM got electrolytes. It’s what plants crave.
Do the opposite of what is right, this is “better programming”. So instead of fixing these design problems, the dude is going to focus on generalizing the view model which has nothing to do with functionalities of
The art of over-generalization
Let’s take a moment to appreciate this gem. This is art.
First you need to create a
coordinator which is a view model that conforms to
Then you pass it to initialize another view model.
Then you pass this view model as a generic parameter to initialize a view.
The process is so tedious you have to build a factory to generate it. And to build this factory you need a
enum type first.
Now consider that you want to use a different view model which uses a different network service.
None of this shit works.
And notice the
Scene1 prefix. This static factory function is dependent on a particular view, for a particular view, and should be static function for that view. You are now writing 2 view models and n factory methods plus 1 enum type for every view. And none of that shit works unless you write more view model types(e.g.;
Scene1ViewModel2) and factory methods.
Are you ever going to swap out for a different coordinator? Unlikely.
How often do you swap out for a different view model? Rarely. You should at least try to design view model to be somewhat configurable.
All this generalization for something you rarely need, and when you need it, you have to hard-coded it in view-specific factory method anyway!
Let me get this straight, not only did this dude write such anti-pattern, he gets to publish it in “better programming” as tutorial, and he gets you to think this is a good idea?
Oh shit, I haven’t even got to the good part.
As in whatever complete opposite of idiomatic Swift.
This is your house built on sand. This is the culmination of anti-patterns.
These things have to happen for you to arrive at this:
- You lack of basic understanding of SwiftUI, model-view binding in particular, you work with model side of a model-view binding. View side is computed property, so every diagram is wrong.
- You need to buy in that dumping everything into a sink object is a good idea
- You fail to see putting model in reference type defeats the purpose of value type based view hierarchy
- You tried to re-create MVVM+C in SwiftUI for buzzwords.
- Somehow you decided a coordinator coupled with view on per-view basis is the best way to do this
- You fell into the trap of over-generalization. So your view has generic parameters complete with hard-coded factory methods.
- None of these shit fix any design problems of Coordinator
- Finally you need type-erased protocol otherwise generics won’t compile; You have to have a view model, and to pass it as generic parameter you need type-erased protocol. In other words, you have no choice, not because this is a protocol-oriented design choice.
Form over substance.
View model protocol is dumb shit because view model is dumb shit. And the fact that you need such a protocol suggests you are over-generalizing, which is dumb shit.
The idiotic Swift.