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.

Differences between MVVM and SwiftUI.

Difference between .NET View and SwiftUI View. Two different technologies means two different architecture approaches.

First of all, thank you Appeloper for all the effort you put on the whole post!

I love this kind of discussion because it always bring good questions and force us to remember why we use what we currently use. Another reason for being on your side is because some people treat the "Clean Architecture" as a bible that shouldn't be questioned and you are bringing those questions.

To be honest, I agree with a lot of things you showed us but I'm still skeptical on how those things would apply for a big project. Imagine an app that has hundreds of entities and complicated logics, wouldn't those translate to thousand lines of code in a single entity?

Just to be clear, I'm asking these questions with a totally open mind, as I said, I think you have something there and I'm trying to sell it in a new project we are building.

Some points that other people brought up:

  • Architectures should be technology agnostic, "MV" is pretty tied to how SwiftUI works.
  • MVVM is easier to implement and separate logic
  • Other arch like VIP/Clean are easier to test business logic and have more reusable components
  • The UI engineer from Apple told that this is a bad approach

Also you are not proposing a whole new architecture, just a way to build apps in SwiftUI.

Some people are very tied to Software Development as it was 50 years ago, with spec documents and boring stuff like that, but on most companies that's not how it works, it's us developers trying to build the next big thing and making it maintainable and testable.

Hi, posted something about that in older posts, for big projects you can have specific models (modules). You never end up with a big objects. I do large projects very easy to do and productive for the team. Scale very well and with clear (true) “separation of the concerns“.

The goal of architecture is to make your life (or your team’s life) easier. If this is not happening, then your architecture has failed.

…and sorry to repeat… Model != Entities

Model objects represent special knowledge and expertise. They hold an application’s data and define the logic that manipulates that data. The model is a class diagram containing the logic, data, algorithms, … of the software.

For example, you can think Apple Platform as a big project:

// Can be internal (folders inside project) or external (sub-modules, packages) if makes sense
// Independent and focused teamwork
// Easy to build, test and maintenance

Contacts (model)
ContactsUI

EventKit (the model)
EventKitUI

Message (the model)
MessageUI

CoreLocation (the model)
CoreLocationUI

Shared
SharedUI

[module]
[module]UI

Or a TV app:

User (model)
UserUI

LiveTV (the model)
LiveTVUI

VODMovies (the model)
VODMoviesUI

VODSeries (the model)
VODSeriesUI

Shared
SharedUI

Everyday I see many, small to big, Clean / VIP / VIPER projects with a massive Entities (…Interactors, Presenters, …) folders without a clear separation of the things where all the team members work on that (all things), creating problematic situations and complexity hell. Recently we watch the bad experience (NSSpain video) from SoundCloud team and how “Uncle Bod” things broke the promise.

I agree with @Appeloper. I started applying what has been said in this thread in a project I started from scratch and it makes a lot more sense. It's not perfect (but what is? Especially sometimes issues with threading and the use of @Published vars) but it makes the code a lot more compact, and I'm pretty sure that if I were to go through the code with @Appeloper there would be more gains made because I just arrived in the land of the "converts".

When you talk about big projects the use of "rigid" design patterns makes things even worse. I had the "luck" of working once in a VIPER code base. OMG. The amount of files is just staggering. If I see now that a company requires knowledge of Clean architecture / VIP / VIPER to work on their stuff I steer away. What a load of bloatware, hundreds and hundreds of files that don't really serve any purpose except for "adhering to the principles". In a way it becomes so "modular" that nobody understands what's going on anymore and lots of files just serve as "data passages" not doing anything useful. And most of the advantages of using this kind of architecture isn't even exploited (e.g. testability, but the entire codebase contains no tests. Or in theory you can swap out an interface layer for another, but let's be honest, how many projects actually have to do this? Especially if you write Swift, it's not as if you are going to plug a HTML frontend on your app all of the sudden).

I'd love if there was a GitHub repo anywhere with a simple version of the principles of this thread - I'm a .net dev moved over to iOS, our main app is mainly converted to SwiftUI but it's all MVVM, viewcontrollers, coordinators and some UIKIT bits so very hard to unpack without being a UIKIT dev previously

I have lots of SwiftUI gaps to fill so a sensible/simple structure to look at would be super useful!

I think you have many examples of it on web and WWDC sessions. There’s another guy leaving out MVVM, maybe he explain with some tutorials next months:

Another guy leaving out MVVM. Is testability really more important than using SwiftUl as intended? …sacrifice a lot of great SwiftUl's built in APIs. From my experience we don’t need VMs (MVVM) for testability or scalability.

Today many people talks about microservice (or modular) architectures. That concept are part of Apple platforms from 90s.

Thanks for this post @Appeloper. My team and me are reading through it and it's making us think a lot about our current practices and the architecture we are using.

There is something that is make me struggle a lot. With this approach your view have freedom to use one or more "Stores" if they need to, this is an important difference regarding MVVM where any view would have her own ViewModel, is this correct?

Then you could end up having something like this:

class AllChannelStore: ObservableObject {
    @Published var channels: [Channel] = []
    @Published var isLoading: Bool = false
    var loadError: Error? = nil

    func load() async {
        isLoading = true
        do {
            channels = try await Channel.channels
        } catch {
            loadError = error
        }
        isLoading = false
    }
}
​
class FavoriteChannelStore: ObservableObject {
    @Published var channels: [Channel] = []
    @Published var isLoading: Bool = false
    var loadError: Error? = nil

    func load() async {
        isLoading = true
        do {
            channels = try await Channel.favoriteChannels
        } catch {
            loadError = error
        }
        isLoading = false
    }
}
​
struct HomeChannelList: View {
    @StateObject private var allChannelStore = AllChannelStore()
    @StateObject private var favoriteChannelStore = FavoriteChannelStore()

    var body: some View {
        if ????isLoading???? {
            ProgressView()
        } else if ????isError???? {
            ErrorView()
        } else {
            ContentView()
        }
        .task {
            await allChannelStore.channels
            await favoriteChannelStore.channels
        }
    }
}

Here we have one view that uses two different stores as source of true since it has to show the Favorite channels, but also All channels. In this scenario how would you control the state the view is in (loading, success, failure, etc)?

My ideas are:

  • We could still have the state in each ObservableObject separately, but then we would need to add a bunch of logic after await favoriteChannelStore.channels to see if any of the two stores has given an error, or maybe (depending on the business logic) it could be okey to render only part of the view if one of the two hasn't failed.
  • Move everything to the view, so that she can control her own state. But I'm afraid that doing so you would end up with views that would controller their state and some other views that would have it controlled by the ObservableObject and I don't like this kind of inconsistencies.
  • Add another Store on top of Favorite channels and All channels store that would control this... which sounds a lot like going back to ViewModels

I have the feeling I'm missing something really basic here, and some insights on it would be really appreciated

Everything depends on your needs. The “store” works like the working memory. We load & aggregate the data we need (to use and manipulate) from different sources. It become the source of truth and can be used (and shared) by many “views”. React / Flutter / SwiftUI patterns are modern evolution of old patterns (e.g. MVVM, MVC, …).

Data

struct Channel: Identifiable, Codable {
    let id: String
    let name: String
    let genre: String
    let logo: URL?

    // Factory Methods
    static var all: [Self] { … }
    static var favorites: [Self] { … }
}

Example 1 - Handle individual sources

class ChannelStore: ObservableObject {
    @Published var channels: [Channel] = []

    @Published var isLoading: Bool = false
    var loadError: Error? = nil

    func loadAll() async {
        isLoading = true
        do {
            channels = try await Channel.all
        } catch {
            loadError = error
        }
        isLoading = false
    }

    func loadFavorites() async {
        isLoading = true
        do {
            channels = try await Channel.favorites
        } catch {
            loadError = error
        }
        isLoading = false
    }
}

Example 2 - Aggregate all related information

class ChannelStore: ObservableObject {
    @Published var channels: [Channel] = []
    @Published var favoriteChannels: [Channel] = []

    @Published var isLoading: Bool = false
    var loadError: Error? = nil

    func load() async {
        isLoading = true
        do {
            channels = try await Channel.all
            favoriteChannels = try await Channel.favorites
        } catch {
            loadError = error
        }
        isLoading = false
    }
}

Hi @Appeloper, I have some questions about the Store in this architecture called MV

  1. As I can see from the example above, your Store really looks like what they called View-Model in the MVVM architecture. From what I understand, the View-Model is exclusively belong to a specific View, while Store can be shared between multiple Views. Is there anything other differents or it's just that?

  2. If we mix UIKit and SwiftUI, I think the Store should be able to be shared with both kind of views, am I correct? I mean, if we use our plain old MVC, it's like your new idea about MV, where Model is still the same, and ViewController means View, and the Store should be able to shared between UIKit's ViewController and SwiftUI's View, right?

Hi Eddie,

  1. YES, correct and more… ViewModel also represents the presentation logic / state like “showConfirmationAlert”, “filterBy”, “sortBy”, “focus”, … In SwiftUI you can use @State properties for that. In MVVM the View state is binding to the ViewModel object. Store is more in “model” (or data) side.

  2. Yes you can use Store in UIKit (MVC) but remember that in Apple MVC the “Store” is what many of us call the “Model Controller”. (e.g. NSFetchedResultsController, NSArrayController, UIDocument, …).

In declarative platforms UI = f(State) (data-driven approach) you don’t need a middle man (controller, viewmodel, …). The ”Stores” are part of the model and works like the working memory, we use it to load / aggregate the data we need to handle (process, work, …).

I call “Store” (from React and WWDC videos) but we can call what we want and makes sense. We should just avoid to mix it with MVVM pattern.

Hi @Appeloper, I have question about MV architecture:

I have app with tabView (4 screens), one of them is called MenuView that contain all app features (more than 10). I want to follow MV architecture I need to declare all stores in menuView like this :

struct MenuView: View {
    @StateObject private var store1 = FeatureOneStore()
    @StateObject private var store2 = FeatureTwoStore()
    @StateObject private var store3 = FeatureThreeStore()
     ....
    
    // mode, ...
    
    var body: some View {
        NavigationStack(path: $navigation.path) {
             VStack {

             }
            .navigationDestination(for: ItemKey.self) { key in
                 // Push to features
            }
        }
        .environmentObject(store1)
        .environmentObject(store2)
        .environmentObject(store3)
     }
}

MenuView will end up with many stores, is that good ? or there is other solution

Hi @Appeloper, in your previous post, you wrote this:

struct Channel: Identifiable, Codable {
    let id: String
    let name: String
    let genre: String
    let logo: URL?

    // Factory Methods
    static var all: [Self] { … }
    static var favorites: [Self] { … }
}

My question is, what if you have multiple "source of truth", e.g. when offline, read from coredata, otherwise when online, read from firebase realtime db. If you write these logic inside the model (Channel), wouldn't it violate the purpose of the Model? Or maybe this is just an example, and in real case, you'd create another layer to retrieve the actual data? (Networking or offline storage)?

My concern is: In a decentralize environment, what if you have a model name ChatMessage, you can retrieve this model from different sources (different host urls, different api paths, local storage), how would you design your Model Object?

Stop using MVVM for SwiftUI
 
 
Q