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
Post
Replies
Boosts
Views
Activity
I’m not calling it, don’t looked for it, just discovered it accidentally and asked why this “c function” that break the app can be called from Swift.
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
}
}
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).
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: [])
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
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.
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.
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.
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()
Hi Eddie,
YES, correct and more… ViewModel also represents the presentation logic / state like “showConfirmationAlert”, “filterBy”, “sortBy”, “focus”, … In SwiftUI you can use @State properties for that. In MVVM the View state is binding to the ViewModel object. Store is more in “model” (or data) side.
Yes you can use Store in UIKit (MVC) but remember that in Apple MVC the “Store” is what many of us call the “Model Controller”. (e.g. NSFetchedResultsController, NSArrayController, UIDocument, …).
In declarative platforms UI = f(State) (data-driven approach) you don’t need a middle man (controller, viewmodel, …). The ”Stores” are part of the model and works like the working memory, we use it to load / aggregate the data we need to handle (process, work, …).
I call “Store” (from React and WWDC videos) but we can call what we want and makes sense. We should just avoid to mix it with MVVM pattern.
Everything depends on your needs. The “store” works like the working memory. We load & aggregate the data we need (to use and manipulate) from different sources. It become the source of truth and can be used (and shared) by many “views”. React / Flutter / SwiftUI patterns are modern evolution of old patterns (e.g. MVVM, MVC, …).
Data
struct Channel: Identifiable, Codable {
let id: String
let name: String
let genre: String
let logo: URL?
// Factory Methods
static var all: [Self] { … }
static var favorites: [Self] { … }
}
Example 1 - Handle individual sources
class ChannelStore: ObservableObject {
@Published var channels: [Channel] = []
@Published var isLoading: Bool = false
var loadError: Error? = nil
func loadAll() async {
isLoading = true
do {
channels = try await Channel.all
} catch {
loadError = error
}
isLoading = false
}
func loadFavorites() async {
isLoading = true
do {
channels = try await Channel.favorites
} catch {
loadError = error
}
isLoading = false
}
}
Example 2 - Aggregate all related information
class ChannelStore: ObservableObject {
@Published var channels: [Channel] = []
@Published var favoriteChannels: [Channel] = []
@Published var isLoading: Bool = false
var loadError: Error? = nil
func load() async {
isLoading = true
do {
channels = try await Channel.all
favoriteChannels = try await Channel.favorites
} catch {
loadError = error
}
isLoading = false
}
}
Today many people talks about microservice (or modular) architectures. That concept are part of Apple platforms from 90s.
Another guy leaving out MVVM. Is testability really more important than using SwiftUl as intended? …sacrifice a lot of great SwiftUl's built in APIs. From my experience we don’t need VMs (MVVM) for testability or scalability.
I think you have many examples of it on web and WWDC sessions. There’s another guy leaving out MVVM, maybe he explain with some tutorials next months: