Stop using MVVM for SwiftUI

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.

Post not yet marked as solved Up vote post of Appeloper Down vote post of Appeloper
85k views
  • 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

Replies

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:

https://github.com/onl1ner/swiftui-mv-architecture

  • 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)

Add a Comment

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?

Add a Comment

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

  • In a sign up page, where should I add the logic for a password strength checker?
  • Or say I have a chat app, I need to paginate and download the previous 100 messages upon scrolling continuously. Where should I keep track of the last seen message and initiate the call to download the messages?

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".

  • If SwiftUI is ready for production? YES
  • Is SwiftUI productive? YES, specially if you follow the new paradigm and ignore old stuff like MVC, MVVM, VIPER, ...
  • Is SwiftUI buggy? YES, some workaround needed for some cases, hope more SwiftUI fixes and integrations at WWDC 2024

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:

  • Simple and straightforward, especially for smaller apps or projects.
  • Views can directly interact with the model, making it easy to understand the flow of data.

Cons of MV Architecture:

  • Can lead to tight coupling between the view and the model, making it harder to test and maintain.
  • Doesn't provide a clear separation of concerns, which can lead to more complex and less maintainable code as the app grows.

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:

  • Promotes a clear separation of concerns: Views are responsible for UI only, ViewModel handles presentation logic, and Model handles data and business logic.
  • Enables easier testing: ViewModels can be unit tested independently of the UI.
  • Facilitates data binding and reactive programming, which aligns well with SwiftUI's declarative nature.

Cons of MVVM Architecture:

  • Adds complexity to the architecture, which might be overkill for very simple apps.
  • Requires additional effort to set up initially compared to the MV 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.