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
84k 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

Simple software design can only be achieved by really fully understanding the problem First you have to really understand what’s going on, in all its complexities, and then come up with a solution so simple that – in hindsight – it is the obvious way to do it.

  • Don't over-engineer your code
  • Don't fall into the tutorial trap
  • Don't just copy and paste code without understanding it
  • Don't blindly follow someone else's strategy
  • Avoid sacrificing software design for testability
  • Consider mocking only as a last resort

Be careful, in an effort to make code more testable, we can very often find ourselves introducing a ton of new protocols and other kinds of abstractions, and end up making our code significantly more complicated.

There's an object for that!

  1. Identify Objects
  2. Identify Tasks
  3. Identify Dependencies
  • Don’t fight the system
  • Don’t literally start with a protocol
  • Avoid unnecessary internal & external dependencies
  • Avoid too many layers of abstraction or complexity
  • Use nested type for supporting or to hide complexity
  • Maybe you can have V-VM-M View-ViewModel-Model.

Add a Comment

We are living in Object Oriented Programming (OOP) world!

  • Don't do Clean / SOLID anti-patterns, be an OOP engineer
  • In the agile world use Feature Driven Development (FDD)
  • Avoid as you can Test Driven Development (TDD) / Extreme Programming (EP)

Yes, Swift is POP (the OOP where you can use structs as objects and protocols for generalization).

  • But if you haven't realized this is the real world, not some classroom and what people decide to use in the real world as architecture, is entirely of their own volition. Peddle your wares somewhere else! Not sure what's up with you but can take this attitude of yours to the classroom where it belongs!

  • Is this from Saturday Night Live skit? Saying no to TDD is just wasting man hours.

  • Managing complexity is the second most important responsibility of software developers. In the last 10 years software has become problematic and expensive… welcome to TDD / XP / SOLID world!

  • In app, you store and process data by using a data model that is separate from its UI
  • Adopt the ObservableObject protocol for model classes
  • Use ObservableObject where you need to manage the life cycle of your data
  • Typically, the ObservableObject is part of your model

StoreKit 2 is a great example how to create your model using Active Record and Factory Method patterns.

StoreKit 2

Remember:

SwiftUI automatically performs most of the work traditionally done by view controllers (view models too).

Declarative UI does not require MVVM. We are now in an era where declarative UI is commonplace in iOS development.

The other things you can do with property wrappers, modifiers and custom views.

Everyday I see bad developers, normally coming from other platforms, ignoring the obvious.

In the field of React/Vue/Flutter, which also uses declarative UI, the architecture of MVVM is not adopted, and it seems strange to adopt MVVM only in SwiftUI.

While I generally see your point and it makes a lot of sense. I am not sure if I fully understand it.

For example how to test apps made with MV pattern?(I am assuming we will be simply testing the whole View, Not that I am particularly against it, simply not sure if this is the right approach)

Also what about creating massive Views? To avoid it you will probably use Observable objects in this case anyway so you will get MVVM?

Part I

Be careful, in an effort to make code more testable, we can very often find ourselves introducing a ton of new protocols and other kinds of abstractions, and end up making our code significantly more complicated. Avoid sacrificing software design for testability and consider mocking only as a last resort.

Model (Data / Networking / Algorithms) objects represent special knowledge and expertise. They hold an application’s data and define the logic that manipulates that data.

The model layer is not simple structs (POJOs). A model is a system of objects (structs / classes) with properties, methods and relationships. For simple apps we can define one model, for complex apps we can have many models as needed (and makes sense).

Popular Model design patterns: Active Record, Data Mapper and Factory Method are the key' to avoid massive ViewControllers / ViewModels (and unnecessary layers).

See “The Features of the Main Data Access Patterns Applied in Software Industry” by Marcelo Rodrigues de Jesus

Model is everything your app (or part / system) can do, in a pure OOP. View is just an interaction layer and handle presentation logic.

See WWDC2010 Session - MVC Essential Design Pattern for Flexible Software See StoreKit 2 documentation for an example of the model of In-App Purchases. See MusicKit documentation for an example of the model of Music Catalog.

Normally Apple split frameworks, but not exclusive, into (e.g.):

  • Contacts (non-UI, model layer)
  • ContactsUI (UI, view layer)

Remember not all your code need tests, people are over-testing today. In many cases if you design the software applying the best patterns you will never need a test at all. Tests, pull-requests, ... are talked today because people don't plan & design software, people create many dependencies & layers making isolations / unchanged code difficult.

Think your software, just focus on your model(s) and your objects!

About the tests:

  • Unit Tests (should used where there's non other objects / systems integration, test your model)
  • Integration Tests (should used where there's other objects / systems integration, test your model)
  • UI Tests (test your UI and presentation logic)

But an ugly guy said: "Ahhh you should do Unit Tests for integrations integrations, use mocks for them" and boom! This guy killed the software design. Wrong!! First you should avoid this and second there's many ways to mock without complicate or sacrifice the software design. Default rule: Use Unit Tests only when you need to test a calculation, algorithm, ... not when you do a request to web service. Don’t fall in unit test troll!

See “Unit Testing is Overrated” by Oleksii Holub

“Unit Tests” project in Xcode is Unit Tests and Integration Tests.

Part II

This is the reason for Massive View Controllers. When you define the model as POJOs (simple structs) you end with many logic inside the ViewControllers / ViewModels. VC and VM should only handle presentation logic. In many cases impossible to test non-ui. Also theres a bad dependencies on you non-ui code. To avoid this you end with unnecessary layers. And… you have to make change for multiplatform views.

But if you go OOP & software design patterns, everything will be perfect. Also, as you see, every object become independent because “Service Objects” just gives the access / door, don’t know what the other objects want. Whenever possible, each object should be concrete and independent. If we add / remove / move a object, the others should keeping working and remaining unchanged. Your model is ready for every platform views, terminal, tests, …

NOTE: Imagine the CLLocationManager using await / async, no delegates. And yes you can do an locations manager if need.

Hope new frameworks this WWDC from ground for Swift and Swift Concurrency.

CLLocation and CLLocationManager just become Location:

  • try await Location.current (gives current user location)
  • for await location in Location.updates (gives every locations changes, async sequence)

Part III

Shopping App

The Model Layer

KISS - Simple design become more easy to do, more easy to maintain / scale, more easy to find & fix bugs, more easy to test.

You can use in UIKit, SwiftUI, iOS, tvOS, macOS, Terminal, Unit Tests, …hey! You can make a Swift Package and share with other apps. It works like magic!

In a big app you can have more models “Workout model” + Nutrition model”, “LiveTV model” + “VideoOnDemand model” + “Network & Subscription model”, …

You can use the “TEST” (like DEBUG) flag for mocks, in WebService provider level but also in your factory methods. Also you can extend the provider and override the request method… there are 1001 ways to mock… but remember don’t sacrifice the simplicity for testing.

The View Layer

A view has a body and handle presentation logic.

SwiftUI automatically performs most of the work traditionally done by view controllers (and view models), but what about the other work like fetch states or pagination? SwiftUI is very good at composable / components and there are modifiers and property wrappers, just use your imagination and the power of the SwiftUI… yes you don’t need extra layers, in last resort make utility objects (e.g. FetchableObject) in your “Shared” folder. With Swift Concurrency everything becomes easy to use and to reuse.

@AsyncState private var products: [Product] = []

List { … }
.asyncState(…)

Example of fetch state property wrapper + modifier.

import SwiftUI

public enum AsyncStatePhase {
    case initial
    case loading
    case empty
    case success(Date)
    case failure(Error)
    
    public var isLoading: Bool {
        if case .loading = self {
            return true
        }
        
        return false
    }
    
    public var lastUpdated: Date? {
        if case let .success(d) = self {
            return d
        }
        
        return nil
    }
    
    public var error: Error? {
        if case let .failure(e) = self {
            return e
        }
        
        return nil
    }
}

extension View {
    @ViewBuilder
    public func asyncState<InitialContent: View,
                           LoadingContent: View,
                           EmptyContent: View,
                           FailureContent: View>(_ phase: AsyncStatePhase,
                                                 initialContent: InitialContent,
                                                 loadingContent: LoadingContent,
                                                 emptyContent: EmptyContent,
                                                 failureContent: FailureContent) -> some View {
        switch phase {
        case .initial:
            initialContent
        case .loading:
            loadingContent
        case .empty:
            emptyContent
        case .success:
            self
        case .failure:
            failureContent
        }
    }
}

@propertyWrapper
public struct AsyncState<Value: Codable>: DynamicProperty {
    @State public var phase: AsyncStatePhase = .initial
    
    @State private var value: Value
    
    public var wrappedValue: Value {
        get { value }
        nonmutating set {
            value = newValue
        }
    }
    
    public var isEmpty: Bool {
        if (value as AnyObject) is NSNull {
            return true
        } else if let val = value as? Array<Any>, val.isEmpty {
            return true
        } else {
            return false
        }
    }
    
    public init(wrappedValue value: Value) {
        self._value = State(initialValue: value)
    }
    
    @State private var retryTask: (() async throws -> Value)? = nil
    
    public func fetch(expiration: TimeInterval = 120, task: @escaping () async throws -> Value) async {
        self.retryTask = nil
        
        if !(phase.lastUpdated?.hasExpired(in: expiration) ?? true) {
            return
        }
        
        Task {
            do {
                phase = .loading
                value = try await task()
                if isEmpty {
                    self.retryTask = task
                    phase = .empty
                } else {
                    phase = .success(Date())
                }
            } catch _ as CancellationError {
                // Keep current state (loading)
            } catch {
                self.retryTask = task
                phase = .failure(error)
            }
        }
    }
    
    public func retry() async {
        guard let task = retryTask else { return }
        await fetch(task: task)
    }
    
    public func hasExpired(in interval: TimeInterval) -> Bool {
        phase.lastUpdated?.hasExpired(in: interval) ?? true
    }
    
    public func invalidate() {
        if case .success = phase {
            phase = .success(.distantPast)
        }
    }
}

extension View {
    @ViewBuilder
    public func asyncState<T: Codable,
                           InitialContent: View,
                           LoadingContent: View,
                           EmptyContent: View,
                           FailureContent: View>(_ state: AsyncState<T>,
                                                 initialContent: InitialContent,
                                                 loadingContent: LoadingContent,
                                                 emptyContent: EmptyContent,
                                                 failureContent: FailureContent) -> some View {
        asyncState(state.phase,
                   initialContent: initialContent,
                   loadingContent: loadingContent,
                   emptyContent: emptyContent,
                   failureContent: failureContent)
    }
}

Hope popular needs / solutions implemented in SwiftUI out of the box.

First of all, thank you for taking the time to post all of this detailed discussion.

I have to admit, I started scanning, perhaps because I'm not sure if this resonated with me entirely.

I like the separation between what a view can do and how a view model responds to actions and is responsible for changing state that the view is ultimately binding.

The view model adapts all of the data it manages to a format the View needs. So I dunno; whether one is using UIKit or SwiftUI, one can take a similar approach.

You could argue this is over-enginering; I consider it a separation of concerns. I personally believe a View should be vapid and vain: as dumb as possible and only interested in how it should look. So, if somebody interacts with it, it just notifies its view model. View Model does the heavy lifting then changes state as required, and the View binds properties that are required for it to look good / right.

  • I feel similar way. I like my views yo just display formatted data, so of the roles of the view model is to prepare "cell models", "child view models", or whatever, that contains already presentable data. I don't like to tie my views to data model. My views can represent any data that comforms to a specific protocols. I find it very good for usability.

  • yep, the UI Layer not manipulating data directly and thus delegating to either controller/view-model is separation of concerns (where business or domain logic doesn't need to be tied(directly) to the views - though they can be packaged separately and expose only public interfaces for consumption by view).

    By doing tight coupling, mocking becomes a problem when doing isolated component testing or integration testing.

  • Also when working in larger organizations, there needs to be some clear boundaries between View(UI)/Model(domain/network/database) logic since each might be managed by different teams and other team won't care mostly unless issue if any is confirmed to be in their domain.

    when there is no such boundaries, mostly particular teams get hit most of the time and after wasting several developer hours will pass on to the proper team.

Add a Comment

I understand your concerns but in case of SwiftUI (declarative) the “MVVM” (original from 2005) is not the correct approach. Maybe the problem is people calling ViewModel and MVVM.

Let me explain, in MVVM almost every View most have a ViewModel, basically you have a middle layer (data binding). In SwiftUI (declarative) you don’t need that, in SwiftUI you can use the concept of “Store” to do the odd jobs. In fact this is a “model type” but we should avoid call ViewModel (and MVVM) because the conflict.

This is MVVM:

HomeView - HomeViewModel
ProductListView - ProductListViewModel
ProductDetailView - ProductDetailViewModel
ProfileView - ProfileViewModel

This is SwiftUI:

CurrentProfile (or in generic Current<Profile>)
ProductStore (or in generic Store<Product>)
AppState / NavigationState

HomeView
ProductListView
ProductDetailView
ProfileView
  1. Focus on the model (data + business logic)
  2. Create views that reflect the model
  3. If needed use a model type (e.g. Store) in your view layer to do odd jobs

But why I’m complain about the use of MVVM for SwiftUI? Because I worked for Microsoft platforms (community leader) and evangelist the MVVM from 2005.

SwiftUI is a modern UI framework and in the declarative world MVVM (original) feels unnecessary and very limited. The problem is people understand what MVVM architecture is in fact. Maybe they are just calling every non-view objects (e.g. store, state, current, …) ViewModels but this is not MVVM.

MVVM vs SwiftUI

Mail app

App Store app

Shopping app

  • Where you got these slides from? Is there code comparing these implementations?

Add a Comment

For me ... Testing - UI (Compose / SwiftUI) (instrument) & Unit & Integration & End2End & CI/ CD is very important. It is the foundation of Clean Arch.

And you are not thinking long term ??? KMM!
https://kotlinlang.org/lp/mobile/

https://kotlinlang.org/docs/multiplatform-mobile-integrate-in-existing-app.html#what-else-to-share

https://github.com/Kotlin/kmm-integration-sample/blob/master/app/src/main/java/com/jetbrains/simplelogin/androidapp/ui/login/LoginViewModel.kt

~Ash