Don’t over-engineering! No suggested architecture for SwiftUI, just MVC without the C.
On SwiftUI you get extra (or wrong) work and complexity for no benefits. Don’t fight the system.
Don’t over-engineering! No suggested architecture for SwiftUI, just MVC without the C.
On SwiftUI you get extra (or wrong) work and complexity for no benefits. Don’t fight the system.
When I started learning SwiftUI in 2019, I adopted MVVM pattern for my SwiftUI architecture. Most of my apps were client/server based, which means they were consuming a JSON API.
Each time I added a view, I also added a view model for that view. Even though the source of truth never changed. The source of truth was still the server. This is a very important point as source of truth plays an important role in SwiftUI applications. As new screens were added, new view models were also added for each screen. And before I know it, I was dealing with dozens of view models, each still communicating with the same server and retrieving the information. The actual request was initiated and handled by the HTTPClient/Webservice layer.
Even with a medium sized apps with 10-15 screens, it was becoming hard to manage all the view models. I was also having issues with access values from EnvironmentObjects. This is because @EnvironmentObject
property wrapper is not available inside the view models.
After a lot of research, experimentation I later concluded that view models for each screen is not required when building SwiftUI applications. If my view needs data then it should be given to the view directly. There are many ways to accomplish it. Below, my view is consuming the data from a JSON API. The view uses the HTTPClient to fetch the data. HTTPClient is completely stateless, it is just used to perform a network call, decode the response and give the results to the caller. I use this technique when only a particular view is interested in the data.
This is shown in the implementation below:
struct ConferenceListScreen: View {
@Environment(\.httpClient) private var httpClient
@State private var conferences: [Conference] = []
private func loadConferences() async {
let resource = Resource(url: Constants.Urls.conferences, modelType: [Conference].self)
do {
conferences = try await httpClient.load(resource)
} catch {
// show error view
print(error.localizedDescription)
}
}
var body: some View {
Group {
if conferences.isEmpty {
ProgressView()
} else {
List(conferences) { conference in
NavigationLink(value: conference) {
ConferenceCellView(conference: conference)
}
}
.listStyle(.plain)
}
}
.task {
await loadConferences()
}
}
}
Sometimes, we need to fetch the data and then hold on to it so other views can also access and even modify the data. For those cases, we can use @Binding to send the data to the child view or even put the data in @EnvironmentObject. The @EnvironmentObject implementation is shown below:
class StoreModel: ObservableObject {
private var storeHTTPClient: StoreHTTPClient
init(storeHTTPClient: StoreHTTPClient) {
self.storeHTTPClient = storeHTTPClient
}
@Published var products: [Product] = []
@Published var categories: [Category] = []
func addProduct(_ product: Product) async throws {
try await storeHTTPClient.addProduct(product)
}
func populateProducts() async throws {
self.products = try await storeHTTPClient.loadProducts()
}
}
Inject it into the root view as shown below:
@main
struct StoreAppApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(StoreModel(client: StoreHTTPClient()))
}
}
}
And then access it in the view as shown below:
struct ContentView: View {
@EnvironmentObject private var model: StoreModel
var body: some View {
ProductListView(products: model.products)
.task {
do {
try await model.populateProducts()
} catch {
print(error.localizedDescription)
}
}
}
}
Now, when most readers read the above code they say "Oh you change the name of view model to StoreModel or DataStore etc and thats it". NO! Look carefully. We are no longer creating view model per screen. We are creating a single thing that maintains an entire state of the application. I am calling that thing StoreModel (E-Commerce) but you can call it anything you want. You can call it DataStore etc.
The main point is that when working with SwiftUI applications, the view is already a view model so you don't have to add another layer of redirection.
Your next question might be what about larger apps! Great question! I have written a very detailed article on SwiftUI Architecture that you can read below:
NOTE: I also covered testing in my article so make sure to read that too.
https://azamsharp.com/2023/02/28/building-large-scale-apps-swiftui.html
I have also written a detailed article on SwiftData. The same concepts can be applied when building Core Data applications.
https://azamsharp.com/2023/07/04/the-ultimate-swift-data-guide.html
NOTE: Appeloper is 100% correct. You don't need view models per screen when building SwiftUI applications.
Isn't your StoreModel still a ViewModel just named differently though?
When most readers read the above code they say "Oh you change the name of view model to StoreModel or DataStore etc and thats it". NO! Look carefully. We are no longer creating view model per screen. We are creating a single thing that maintains an entire state of the application. I am calling that thing StoreModel (E-Commerce) but you can call it anything you want. You can call it DataStore etc. I explained it in detail in my article linked in my post.
This means in a client/server app, where server is the source of truth you will have a single place to handle the entire state of your app (For bigger apps you can create more based on bounded context and domain - read my article linked above).
MVVM approach -> CategoryListViewModel, CategoryDetailViewModel, ProductViewModel, AddCategoryViewModel, AddProductViewModel MV approach -> StoreModel
Also, all view specific logic will go in the View.
Hey there @Appeloper, great thread, thanks for your work! Felt lonely, thinking that way and glad to find a like-minded person. I have created a GitHub repository about this approach on developing SwiftUI apps, would appreciate your contribution:
Finally someone did this, although I think @Appeloper could've contributed with a git repo from the beginning. My greatest concern when thinking about this architecture is how to handle API/external calls and which patterns would fit it. I also think that this thread is a gem, but still a brute one, we, as a community, should improve it and show how this is a great way to solve problems. (I also was glad when I first saw this thread and found like-minded people)
This is an oversimplification of things. If everyone had followed your analysis, we would still be using MVC. As real life proved, it was unbelievable, and everyone got Massive View Controllers. You are saying that Apple considers the MV architecture the next big thing.
Again, this is not a real-life situation. Of course, you can write spaghetti code, don't rely on separation of concerns or anything like that. In the end, we will be in trouble. I still prefer to move the business logic out of the view and models. I still couldn't find anything better than the 3 tier model.
It doesn't make sense to me, and I've been using MVVM very well with SwiftUI, so it's no big deal at all. I still think VIPER and others are overkill, but what you are proposing is on the other extreme of oversimplification.
There are also a lot of misconceptions in your post. For example, saying that MVVM is pre-reactive leads people to think that you should not / cannot achieve Reactive flow using MVVM, which is false.
If it doesn't make sense the first thing to learn (and Apple don't do a great job of explaining this) that the View struct hierarchy with its dependency tracking and diffing is the view model already. If you ignore that and use your own view model objects then you'll likely have the same consistency bugs that SwiftUIs implementation using structs was designed to eliminate. It's very tempting to use familiar objects but it really is worth putting the effort in and learning to use View structs.
When it detects a change in data, SwiftUI makes new View structs and diffs then with the previous ones. That difference is used to drive initing/updating/deallocing UIKit objects. Hopefully that helps you understand View structs are the view model.
@malc "View struct hierarchy with its dependency tracking and diffing is the view model already" what if I work on an iPhone, iPad, and MacOS app? 3 different UIs and the same business logic? With MVVM I'd have 3 different Views that share the same VM. How would I achieve that with MV(or whatever it is) approach? Also how do you unit test your logic if it's inside a View?
Theres just one caveat to all of this.
With swift observation, all of those ObservableObject
issues go away.
The observation framework is smart enough to only update the views that are actually observing a value (just like @State
does). Rather than every view (and all of its children) that has an @ObservedObject/@EnvironmentObject/@StateObject
in it.
So for projects supporting iOS 17 and newer, MVVM is probably valid.
Also Pointfree back ported Observation to iOS 13. So theoretically, you can use all of this new observation stuff right now.
I feel like removing VMs would create some kind of mess.
And I don't think I can agree to your POV that apps are just a trivial function of states. Maybe you can explain what I need to do in these scenarios
Hi guys,
Last week my team launch one of the first big SwiftUI app, ERAKULIS almost 99.8% SwiftUI (no UIKit needed at all). Modular and following MV (no one ViewModel) architecture. Simple and uncomplicated, no VM or other layer needed at all. Just UI and non-UI objects.
I will share more details later but think about "Store<T>" that works like magic (like what @Query do for local data but for remote data) and some "manager objects".
Observables solves many problems but only iOS 17+
When comparing MV (Model-View) architecture to MVVM (Model-View-ViewModel) architecture in a SwiftUI app, it's important to understand the differences in structure and how they impact development and maintenance of the app.
Model-View (MV) Architecture:
In the Model-View architecture, you typically have two main components:
Model: Represents the data and business logic of the application. It encapsulates the data and provides methods to manipulate that data.
View: Represents the user interface components that display the data and interact with the user. In SwiftUI, views are often composed of smaller, reusable components.
Pros of MV Architecture:
Cons of MV Architecture:
Model-View-ViewModel (MVVM) Architecture:
MVVM is an architectural pattern that builds upon the Model-View concept by introducing a new component called ViewModel:
Model: Represents the data and business logic, similar to MV architecture.
View: Represents the user interface, but in MVVM, views are kept as lightweight as possible. Views in MVVM are responsible for displaying data and forwarding user input to the ViewModel.
ViewModel: Acts as an intermediary between the Model and the View. It exposes data and commands that the View can bind to and observe. The ViewModel also encapsulates the presentation logic and state management.
Pros of MVVM Architecture:
Cons of MVVM Architecture:
Choosing Between MV and MVVM for SwiftUI:
For small and simple SwiftUI apps, using a basic MV architecture might be sufficient, especially if you're just starting out with SwiftUI. However, as the complexity of the app increases, and you find yourself needing more separation of concerns, MVVM can be a better choice. MVVM works particularly well with SwiftUI's data binding and state management features, making it easier to build scalable and maintainable apps.
Ultimately, the choice between MV and MVVM depends on the specific requirements of your project, your team's familiarity with architectural patterns, and the expected future growth of the app. MVVM is a more robust and scalable architecture for larger SwiftUI apps, whereas MV might suffice for simpler projects or when learning SwiftUI concepts.
Thank you, very helpful. Especially enjoyed the big fat mess as the end 😂
1/N Thanks for writing this post! I have written many articles regarding SwiftUI and MVVM, I have written books and even created courses on SwiftUI and MVVM. But each time I implemented a solution in SwiftUI and MVVM I felt not comfortable. The main reason was that I was always fighting the framework. When you work on a large application using MVVM with SwiftUI and you want to share state then it becomes extremely hard.
2/N The main reason is that you cannot access EnvironmentObject inside the view model. Well, you can pass it using a constructor but then it becomes pain to do it again and again. And most of the time, you end up getting confused that who is managing the state. I ran into this scenario multiple times and I had to take alternate routes to resolve this problem. One thing to note is that React and Flutter (as original poster said) does not use MVVM pattern. React uses context or Redux and Flutter