Post

Replies

Boosts

Views

Activity

Reply to Stop using MVVM for SwiftUI
Hi Eddie, Normally network data model != database (core data) model. You should have a separated data model and from my experience this save you from many problems. Also any networking cache should be done with default http, let the system do it, or custom saving the response on disk. Keeping the some model data you can do this (using Active Record): struct Channel: Identifiable, Codable { let id: String let name: String let genre: String let logo: URL? static func saveChannels(_ channels: [Self], on: …) async throws { … } // Factory Methods (set the data source object or protocol) static func all(on: …) async throws -> [Self] { … } static func favorites(on: …) async throws -> [Self] { … } } Using repository / manager pattern: struct Channel: Identifiable, Codable { let id: String let name: String let genre: String let logo: URL? } protocol ChannelManager { static func loadAllChannels() async throws -> [Channel] static func loadFavoriteChannels() async throws -> [Channel] static func saveChannels(_ channels: [Channel]) async throws } struct NetworkChannelManager: ChannelManager { static func loadAllChannels() async throws -> [Channel] { … } static func loadFavoriteChannels() async throws -> [Channel] { … } static func saveChannels(_ channels: [Channel]) async throws { … } } struct LocalChannelManager: ChannelManager { static func loadAllChannels() async throws -> [Channel] { … } static func loadFavoriteChannels() async throws -> [Channel] { … } static func saveChannels(_ channels: [Channel]) async throws { … } } Example from Vapor platform: Uses Active Record pattern for data access where you can set the data source. // An example of Fluent's query API. let planets = try await Planet.query(on: database) .filter(\.$type == .gasGiant) .sort(\.$name) .with(\.$star) .all() // Fetches all planets. let planets = try await Planet.query(on: database).all()
Topic: UI Frameworks SubTopic: SwiftUI Tags:
Mar ’23
Reply to Stop using MVVM for SwiftUI
Software models are ways of expressing a software design, e.g. the Channel struct represents the model of a tv channel, the Program struct represents the model of a tv program, the folder / module / package LiveTV (that contains the Channel and Program structs) represents the model of a live tv system. As said before, network data model != local database (core data) model. Also SwiftUI has a great integration with core data.
Topic: UI Frameworks SubTopic: SwiftUI Tags:
Mar ’23
Reply to Stop using MVVM for SwiftUI
Hi OuSS_90, One fast way to fix the problema is to put the alert modifier on NavigationStack and not inside each view: struct PaymentView: View { @StateObject private var store = PaymentStore() var body: some View { NavigationStack { PaymentCreditorListView() /* -> PaymentFormView() */ /* -> PaymentUnpaidView() */ /* -> PaymentConfirmationView() */ } .sheet(item: $store.popup) { popup in PopupView(popup: popup) } .environmentObject(store) } } But from your PaymentStore I think you are mixing different things or lazy load the store information. I don’t forget that store is about data not view (e.g. there’s an error info not a “popup”). Store should be as possible an data aggregator for some domain. I don’t know the needs but here’s a example: class PaymentStore: ObservableObject { @Published var isLoading = false @Published var loadError: Error? = nil private let service: PaymentService @Published var creditors: [Creditor] = [] @Published var form: PaymentForm? = nil @Published var unpaid: ? = ? // Don’t know the type init(service: PaymentService = PaymentService()) { self.service = service } func load() async { isLoading = true do { let creatorsResponse = try await service.fetchPaymentCreditors() let formResponse = try await service.fetchPaymentForm() let unpaidResponse = try await service.fetchPaymentUnpaid() // jsonapi spec creators = creatorsResponse.data form = formResponse.data unpaid = unpaidResponse.data } catch { loadError = error } isLoading = false } } Or split the PaymentStore into CreditorStore, PaymentForm and UnpaidStore, all depends on use case and how data is handled.
Topic: UI Frameworks SubTopic: SwiftUI Tags:
Mar ’23
Reply to Stop using MVVM for SwiftUI
Making a simple social network app from Apple WWDCs CoreData example, in this case the data model is defined in the backend. The use case, what user can do in the system and the dependencies. ERD of database and REST API endpoints Now the data access model (API integration) in Swift. In case the data model defined in app you use the CoreData stack + objects and this is your model. Here you can do Unit / Integration tests. In this case the data are external and you need a model to load (or aggregate) the data in memory and use it: PostStore and TagStore. In case of local data (using CoreData) you don’t need the stores, use the SwiftUI features.
Topic: UI Frameworks SubTopic: SwiftUI Tags:
Mar ’23
Reply to Stop using MVVM for SwiftUI
By design you can only had one alert at the time. Alerts are more for situations like user actions like send / submit, for the loading data cases you should use empty states. See other apps when a network error appear or no data loaded all at. NavigationStack path is all about data, think data, not views. SwiftUI is data-driven nature. Sometimes it feels we are doing MVVM but is different. In classic MVVM ViewModel (presentation logic) handle every View logic & state, each View have one ViewModel. The MVVM used in other platforms last years are not the true MVVM… sometimes ViewModel acts like a Store. ViewModel -> Lives in a middle layer, presentation logic, a Controller with databinding. Store -> Lives in a model layer, data logic
Topic: UI Frameworks SubTopic: SwiftUI Tags:
Mar ’23
Reply to Stop using MVVM for SwiftUI
Hi OuSS_90, For the case you have a single source of truth and progressive / lazy loading, you can have multiple errors (or popups) in the store. Example: class PaymentStore: ObservableObject { @Published var creditors: [Creditor] = [] @Published var form: PaymentForm? = nil @Published var unpaid: PaymentUnpaid? = nil @Published var isLoadingCreditors: Bool = false @Published var isLoadingForm: Bool = false @Published var isLoadingUnpaid: Bool = false @Published var creditorsError: Error? = nil // Popup @Published var formError: Error? = nil // Popup @Published var unpaidError: Error? = nil // Popup private let service: PaymentService init(service: PaymentService = PaymentService()) { self.service = service } func loadCreditors() async { creditorsError = nil isLoadingCreditors = true do { creditors = try await service.fetchPaymentCreditors() } catch { loadCreditorsError = error } isLoadingCreditors = false } func loadForm() async { formError = nil isLoadingForm = true do { form = try await service.fetchPaymentForm() } catch { loadFormError = error } isLoadingForm = false } func loadUnpaid() async { unpaidError = nil isLoadingUnpaid = true do { unpaid = try await service.fetchPaymentUnpaid() } catch { loadUnpaidError = error } isLoadingUnpaid = false } } Also you can have have an enum for load / error state: enum State { case unloaded case loading case success case failure(Error) } … @Published var creditorsState: State = .unloaded Or some envelop for network data: struct AsyncData<T> { var data: T var isLoading! bool = false var error: Error? = nil } … @Published var creditors: AsyncData<[Creditor]>= AsyncData(data: [])
Topic: UI Frameworks SubTopic: SwiftUI Tags:
Apr ’23
Reply to Stop using MVVM for SwiftUI
The MVVM used in other platforms (Android, UIKit) last years isn’t the classic MVVM. What I read on most SwiftUI MVVM blog posts, they use the “Store pattern” but call it MVVM. ViewModel -> Middle layer object, view dependent, presentation logic Store -> Model object, view independent, reusable, shareable, data logic In a real and big / complex project we have 7 Stores, if we followed the “classic” MVVM we end up with +40 ViewModels. We can use e.g. a ProductStore or CategoryStore in many Views, we can share the UserStore or ShoppingCart with many views in hierarchy. Many people do it but call it “ViewModel” (incorrect name).
Topic: UI Frameworks SubTopic: SwiftUI Tags:
Apr ’23
Reply to Stop using MVVM for SwiftUI
Hi jaja_etx, you can share the BookStore in the hierarchy with @EnvironmentObject or @ObservedObject and update the book. // Books view struct BookList: View { @StateObject private var store = BookStore() var body: some View { NavigationStack { List { ... } .task { await store.load() } } .environmentObject(store) } } // Book detail view struct BookView: View { @State var book: Book @EnvironmentObject private var store: BookStore var body: some View { ScrollView { ... } } func toogleIsReaded() { book.isReaded.toggle() // Update the local state store.update(book) // Update the book on the store and send update to the web service if needed } }
Topic: UI Frameworks SubTopic: SwiftUI Tags:
Apr ’23
Reply to Stop using MVVM for SwiftUI
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: Recipe.swift // Object + Tasks Two Recipe files, if needed: Recipe.swift // Object Recipe+Tasks.swift // Tasks The files we need for Recipe: Recipe.swift // Object Recipe+Creating.swift // Creating tasks Recipe+Loading.swift // Loading tasks Recipe+Updating.swift // Updating tasks Recipe+Information.swift // Information tasks
Topic: UI Frameworks SubTopic: SwiftUI Tags:
Jul ’23