I stared working on an open source project called "HelloMarket". It is an E-Commerce application using SwiftUI, ExpressJS and Postgres database. Currently, it is in development phase but you can explore the repository.
https://github.com/azamsharpschool/HelloMarket
Post
Replies
Boosts
Views
Activity
@Luccif
One of the solution is to put domain logic right inside the SwiftData model. This is shown below:
@Model
class BudgetCategory {
@Attribute(.unique) var title: String = ""
var amount: Decimal = 0.0
@Relationship(deleteRule: .cascade) var transactions: [Transaction] = []
init(title: String, amount: Decimal) {
self.title = title
self.amount = amount
}
// exists function to check if title already exist or not
private func exists(context: ModelContext, title: String) -> Bool {
let predicate = #Predicate<BudgetCategory> { $0.title == title }
let descriptor = FetchDescriptor(predicate: predicate)
do {
let result = try context.fetch(descriptor)
return !result.isEmpty ? true: false
} catch {
return false
}
}
func save(context: ModelContext) throws {
// find if the budget category with the same name already exists
if !exists(context: context, title: self.title) {
// save it
context.insert(self)
} else {
// do something else
throw BudgetCategoryError.titleAlreadyExist
}
}
}
Source: https://azamsharp.com/2023/07/04/the-ultimate-swift-data-guide.html
You can also watch my video on SwiftData Testing Domain Models.
https://www.youtube.com/watch?v=OF7TLbMu1ZQ&t=389s
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.
Thanks! Yes. I checked the casing and I made sure that the actorName is associated with a movie.
I agree! There has to be a better way. Since if I am using hexColor then I can simply save hexColor in the database or hex code and construct Color from that. I think we have to somehow make Color or UIColor conform to codable.
Thanks!
This message is from the original poster of this post. If you get this message please contact me on Twitter at @azamsharp. I am writing a post about the same topic and your help will be really appreciated.
4/N Although you can use Redux with SwiftUI but Redux have a lot of moving parts. You can simply create a fetchService, which loads the data and make sure to put that fetchService in environmentObject. This allows it to create one single source of truth. Another comment about Model objects. Most of the time, your app in consuming an API. The data you get from the API is not the domain model, that is the DTO (Data Transfer Object). Your domain object lies on the server, most of the time.
3/N Flutter uses bloc, provider etc. But the main idea is the same. All these frameworks works using the uni-directional flow. So, it would be worth it to use EnvironmentObject and just allow EnvironmentObject to be available to all the views. One complain about EnvironmentObject is that it will refresh a lot of views when any value changes. In those cases you can slice up the environment object (https://azamsharp.com/2022/07/01/slicing-environment-object.html).
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
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.