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
Post
Replies
Boosts
Views
Activity
Hi @Appeloper, Im trying to use MV architecture in my project, below is a simple example of my code:
struct PaymentView: View {
@StateObject private var store = PaymentStore()
var body: some View {
NavigationStack {
PaymentCreditorListView()
/* -> PaymentFormView() */
/* -> PaymentUnpaidView() */
/* -> PaymentConfirmationView() */
}
.environmentObject(store)
}
}
class PaymentStore: ObservableObject {
....
@Published var isLoading = false
@Published var popup: Popup?
private let service: PaymentService
init(service: PaymentService = PaymentService()) {
self.service = service
}
func getPaymentCreditors() async {
do {
isLoading = true
let response = try await service.fetchPaymentCreditors()
.....
isLoading = false
} catch {
isLoading = false
popup = .init(error: error)
}
}
func getPaymentForm() async {
do {
isLoading = true
let response = try await service.fetchPaymentForm()
....
isLoading = false
} catch {
isLoading = false
popup = .init(error: error)
}
}
func getPaymentUnpaid() async {
do {
isLoading = true
let response = try await service.fetchPaymentUnpaid()
.....
isLoading = false
} catch {
isLoading = false
popup = .init(error: error)
}
}
}
On each view I use sheet to show popup error because sometimes I need to do something specific for that view (for ex: calling web service or redirection etc...)
.sheet(item: $store.popup) { popup in
PopupView(popup: popup)
}
The only problem I have right now is when one of the endpoints return an error, all the views that use the popups are triggred and I'm getting this warning message in the console "Attempt to present * on * which is already presenting...", same problem for progressLoader, it will fire all the other views.
Did I miss something with this approach ? or is there a better way to do it ?
Thank you @Appeloper for your reply,
store is about data not view (e.g. there’s an error info not a “popup”) : popup is just a struct that handle error & success messages
enum PopupType {
case success
case failure(Int)
case info
case warning
case custom(String)
}
struct Popup: Identifiable {
var id = UUID()
var type: PopupType
var title: String?
var message: String?
var closeText: String?
var confirmText: String?
}
func load() async {
isLoading = true
do {
let creatorsResponse = try await service.fetchPaymentCreditors()
let formResponse = try await service.fetchPaymentForm()
let unpaidResponse = try await service.fetchPaymentUnpaid()
creators = creatorsResponse.data
form = formResponse.data
unpaid = unpaidResponse.data
} catch {
loadError = error
}
isLoading = false
}
I don't want to call all endpoints one after other, because I have three screens :
PaymentCreditorListView --> call fetchPaymentCreditors() and after user choose creditor I need to call fetchPaymentForm() that take creditor as parameter and then push to PaymentFormView (I need to save creditor to use it later in PaymentConfirmationView)
PaymentFormView --> When user press continue I need to call fetchPaymentUnpaid() that take form info as parameter and then push to PaymentUnpaidView() (I need to save form info & unpaid list to use it later in PaymentConfirmationView)
How can I handle this with their popups for each view using PaymentStore ? and if I need to split it as you said, we will not return to MVVM each view has his own store ?? because as soon as we have many endpoint, it become hard to handle each popup without firing others because they share same publisher
Also how can I handle push after endpoint finish if I can't add navigationPath inside store (because you said store have only Data)
Thank you
The problem I have is how I can handle errors/success for each view, because sometimes after error or success confirmation I need to do something different for some views. it will not work as I want if I add one sheet in root, let me give you an example:
In PaymentFormView I have button that display OTPView, after user enter code I call addToFavorite endpoint that get favoriteName from that view and if the code is wrong addToFavorite throw error, and when user confirm I need to display again OTPView and if it success I display success popup and after confirmation I need to pop to first view.
In PaymentConfirmView I have other scenario, I call submit endpoint and then I display success popup and after confirmation I need to push to other view
As you can see each view have a different staff to do after popup confirmation, If I add one sheet in root, is impossible to do this.
Is it a good idea to move do catch to view instead of store ??
class PaymentStore: ObservableObject {
@Published var creditors: [Creditor] = []
@Published var form: PaymentForm?
@Published var unpaid: PaymentUnpaid?
private let service: PaymentService
init(service: PaymentService = PaymentService()) {
self.service = service
}
func getPaymentCreditors() async throws {
creditors = try await service.fetchPaymentCreditors()
}
func getPaymentForm() async throws {
form = try await service.fetchPaymentForm()
}
func getPaymentUnpaid() async throws {
unpaid = try await service.fetchPaymentUnpaid()
}
}
struct PaymentCreditorListView: View {
@EnvironmentObject private var store: PaymentStore
@State private var idLoading = false
@State private var popup: Popup?
var body: some View {
VStack {
}
.task {
do {
isLoading = true
try await store.fetchPaymentCreditors()
isLoading = false
} catch {
isLoading = false
popup = .init(error: error)
}
.progress($isLoading)
}
}