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.

I wish real software was as simple as just loading data into an array to be displayed in a list as you seem to love to trivialise.

And what are store entities class Store: ObservableObject all about?

Lots of @Published properties and some services, they seem to share a lot with the good old ViewModels, but now the view can have multiple Stores, and that's better how?

Turns out real software is a bit more complex, eh?

The MVVM used in other platforms (Android, UIKit) last years isn’t the classic MVVM. What I read on most SwiftUI MVVM blog posts, they use the “Store pattern” but call it MVVM.

  • ViewModel -> Middle layer object, view dependent, presentation logic
  • Store -> Model object, view independent, reusable, shareable, data logic

In a real and big / complex project we have 7 Stores, if we followed the “classic” MVVM we end up with +40 ViewModels. We can use e.g. a ProductStore or CategoryStore in many Views, we can share the UserStore or ShoppingCart with many views in hierarchy. Many people do it but call it “ViewModel” (incorrect name).

How would you do to make a change on book from BookView and have that change reflect all the way up to BookList?

Hi jaja_etx, you can share the BookStore in the hierarchy with @EnvironmentObject or @ObservedObject and update the book.

// Books view
struct BookList: View {
    @StateObject private var store = BookStore()
    
    var body: some View {
        NavigationStack {
            List {
                ...
            }
            .task {
                await store.load()
            }
        }
        .environmentObject(store)
    }
}

// Book detail view
struct BookView: View {
    @State var book: Book
    @EnvironmentObject private var store: BookStore
    
    var body: some View {
        ScrollView {
            ...
        }
    }
    
    func toogleIsReaded() {
        book.isReaded.toggle() // Update the local state
        store.update(book) // Update the book on the store and send update to the web service if needed
    }
}

One of the coolest threads I've read. Reminds me of Neo, who escaped from the Matrix.

At the WWDC 2023 presentation, a new Observation framework and wrapper over CoreData, SwiftData, was introduced. After practicing with this new design pattern, I realized what the topic starter meant @Appeloper

Using ActiveRecord with SwiftData

Everything is simple and SwiftUI + UnitTest friendly. Remember that we can use SwiftData data in memory, not only in disk. Also we can use it for Web Service data requests creating a ”manager object” to sync Web Service data with our local data.

@Model
class Recipe {
    var name: String
    var description: String
    var ingredients: [Ingredient]
    var steps: [String]
}

// Creating data (factory methods)
extension Recipe {
    static var myFavorite: Self { ... }
    static var top30GordonRamsayRecipes: [Self] { ... }
    
    static func chatGPTSuggestion(text: String) -> Self { ... }
}

// Loading data (factory fetch descriptors)
extension Recipe {
    // @Query(Recipe.allRecipes) private var recipes: [Recipe] // Managed by SwiftUI
    // let recipes = try context.fetch(Recipe.allRecipes) // Managed by developer
    static var allRecipes: FetchDescriptor<Self> { ... }
    static var healthyRecipes: FetchDescriptor<Self> { ... }
    static func recipesWithIngredients(_ ingredients: [Ingredient]) -> FetchDescriptor<Self> { ... }
}

// Updating data
extension Recipe {
    func addSuggestedCondiments() { ... }
    func replaceUnhealthyIngredients() { ... }
    func reduceCaloriesByReduceOrReplaceIngredients(maxCalories: Double) { ... }
    
    func insert(context: ModelContext) { ... }
    func delete(context: ModelContext) { ... }
}

// Information
extension Recipe {
    var totalCalories: Double { ... }
    var isHealthy: Bool { ... }
}

---

@Model
class Ingredient { ... }

We can have one Recipe file:

  • Recipe.swift // Object + Tasks

Two Recipe files, if needed:

  • Recipe.swift // Object
  • Recipe+Tasks.swift // Tasks

The files we need for Recipe:

  • Recipe.swift // Object
  • Recipe+Creating.swift // Creating tasks
  • Recipe+Loading.swift // Loading tasks
  • Recipe+Updating.swift // Updating tasks
  • Recipe+Information.swift // Information tasks

@kp2485 that's bad? Separating things so they can be easily maintained / tested is a good thing, actually.

Appreciate your point of view, but this absolutest way of communicating seems counterproductive. A single person cannot simply declare industry-wide standard architectural pattern just "wrong". MVVM is tried and true and SwiftUI is designed to work well with it, as clearly stated by the developer from Apple.

Hello Appeloper! First I just want to say I really do enjoy this approach versus MVVM. Being new to SwiftUI and seeing the MVVM approach it seemed so bloated and incredibly counter to what I saw in WWDC talks in their more simple demos (very much draw the rest of the owl).

I want to ask about what is the best approach for a Store approach when you have an Account object that retrieves information about a user (in my case firebase auth) and then uses an identifier in that auth information as a path identifier for another store (e.g. firestore).

I have a collection for say Books/<userId>/ and want to have a bookstore as you've described. Do you simply pass in the userId to the .loadStore function? Or should the BookStore contain a reference itself to the underlying auth provider?

@MainActor
class Account: ObservableObject {
    
    @Published var isLogged: Bool = false
    @Published var userDetails: UserDetails? = nil
    @Published var error: Error? = nil
    
    private var firebaseUser: User?
    private var authStateChangeHandler: AuthStateChangeHandler?
    
    private var userService: UserService = UserService.shared
    private var authService: AuthenticationService = AuthenticationService.shared
    
    private let logger = Logger(subsystem: "...", category: "Account")
    
    init() {
        authService.subscribeAuthStateChange { [weak self] (user: User?) in
            guard let self = self else { return }
            if let user = user {
                firebaseUser = user
                isLogged = true
            } else {
                firebaseUser = nil
                isLogged = false
            }
        }
    }
    
    func login(email: String, password: String) async {
        do {
            try await authService.login(email: email, password: password)
        } catch {
            self.error = error
        }
    }
    
    func register(email: String, password: String) async {
        do {
            try await authService.register(email: email, password: password)
        } catch {
            self.error = error
        }
    }
    
    func loadUser() async {
        do {
            guard let firebaseUser = firebaseUser else {
                throw UserError.noUserId
            }
            userDetails = try await userService.fetchUser(with: firebaseUser.uid)
            error = nil
            logger.info("User loaded \(firebaseUser)")
        } catch {
            self.error = error
        }
    }
}

class FirebaseAuthProvider: AuthenticationProvider {
    private let auth = Auth.auth() // <- should this become shared and used inside the stores?
}

Hi @Appeloper, I have one question that has not been asked yet (or I have not seen it). You say that presentation logic now should be inside of the SwiftUI View instead of ViewModel/Presenter as it is in MVVM/MVP.

If I have User model (Active record), and there is a birthday attribute I would like to format it to string by using DateFormatter where should it be? Should I put DateFormatter inside of SwiftUI View? Should it be inside of UserStore? or anywhere else.

It seems kind of odd to me to put DateFormatter inside of the SwiftUI View. That's a simple data formatting, usually, we have more complicated data-view transformations. I do agree with you on many thoughts and ideas, however, I am missing "presentation layer" for a specific view. Something like View - Presentation - Model. (Though, I like the store idea that is agnostic of specific view and I am going to start using it.)

I understand that many times it is not needed however when presentation logic (data transformation to view representable data) becomes more complex so the View as well. For me, view and presentation are two different responsibilities. View should be responsible for how the view is rendered and composed, animations etc. And presentation should be about transforming data from model to view.

This is a fascinating discussion. However, I'm afraid I have to disagree with no VMs stance. Having worked on some pretty large apps built on SwiftUI - we hardly ever deal with a simple model-to-view mapping.

The view often comprises data collated from multiple sources (authentication, current contextual activity, A/B testing, remote configurations, user preferences, etc.) There is no one source of truth - there are many.

Secondly, whenever the user performs an action, the app can do many different things in response: E.g. post analytics, save data locally, do network requests, and perform side calculations. Yes, some of that can be abstracted to stores or service classes, but there is no one good place to orchestrate this flow.

This is where VMs excel. When done properly they are unit testable, predictable and reusable.

However, as always, use the right tool & abstraction for the job. If, for example, the view is a simple 1-to-1 mapping to its model, having a VM is probably overkill.

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.

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

I totally get that MVVM "fights" the framework, but I don't think it's without benefits. Mainly decoupling interaction between data class and view via an observable object view model allows for control of view redraws external to data models. Idk just my thoughts where this is important in the app i'm currently working on - very well could be wrong!

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.

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.

Stop using MVVM for SwiftUI
 
 
Q