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.
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:
Two Recipe files, if needed:
The files we need for Recipe:
@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:
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
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.
Hi @Appeloper, I'm trying to manage my SwiftUI+SwiftData project using the MV approach described in the post. I would like to understand, following this approach, how to handle the case of a view used to edit the swift data object (e.g. Recipe in the previous post) in which it is necessary to perform some operations before actually confirming the saving and updating of the modified fields. Currently my solution is to pass the information into a support observabel class called, perhaps improperly, viewmodel in which there are all the data of the swiftdata object and the saving and removing functions. Is there a more consistent way with the model?
Hi @Chandan_ Why do we always say as a con that there is no "clear separation of concerns". The purpose of this requirement, for practical purposes, is it not enough that the code is correctly ordered, possibly also separating it into multiple files so that any commits from different developers do not touch each other? Is there anything else I don't see? ( @Chandan_ , I tag you, as the last one who wrote it, but equally many others)