Note that a ViewModel is supposed to be specific to a View, if you adopt MVVM. Otherwise, if it is globally available, you may want to name it differently, like a Manager, Observer or whatever is suitable for your model.
In the example below, I am using the name InvoiceManager for the shared Observable instance.
If you want to make an @Observable instance available globally, you have basically two options:
Create an instance of the class in your App struct and share it via the Environment .
Use a singleton that exposes exactly one shared instance of the class.
1. Shared instance via Environment
Consider this simplified InvoiceManager class:
@Observable
final class InvoiceManager {
var notifyUser: Bool = true
func create() {}
}
If you instantiate this class in your root view, like you did in HomeScreen, it will be initialized every time you access the home screen. If you don't want this, you can initialize it in the App struct:
struct DemoInvoiceApp: App {
//State
@State private var invoiceManager: InvoiceManager = InvoiceManager()
var body: some Scene {
WindowGroup {
InvoiceHomeView()
.environment(invoiceManager) // <- make the InvoiceManager instance available via the environment
}
}
}
Note, it's passed to the InvoiceHomeView root view via .environment(invoiceManager).
Now, it can be accessed by the root view, or any of its child views using @Environment:
@Environment(InvoiceManager.self) private var invoiceManager
Using the create() function of the InvoiceManager global instance in the body of the home view becomes as simple as:
invoiceManager.create()
To use it in any other (child) views, you don't need to pass it again via environment (like you would pass a parameter). Any other views can simply grab it from @Environment(InvoiceManager.self) as shown before.
2. Use a singleton pattern
Another way of using a global instance is by having the class use a singleton pattern with a private initializer and a shared static instance.
In this case, the InvoiceManager class would look like this:
@Observable
final class InvoiceManager {
var notifyUser: Bool = true
//Singleton instance
static let shared = SharedInvoiceManager()
//Private init
private init() {}
func create() {}
}
This means that instead of creating an instance using let manager = InvoiceManager(), you would simply access the (only) instance via InvoiceManager.shared:
let invoiceManager = InvoiceManager.shared // not constructed; retrieved
So now accessing the create() function of the InvoiceManager singleton in the body of any view becomes as simple as:
InvoiceManager.shared.create()
Note it is no longer needed to grab it via @Environment.
For clarity, here's what the App struct would look like if using a singleton:
struct DemoInvoiceApp: App {
//Initialize singleton
let invoiceManager: InvoiceManager = InvoiceManager.shared
var body: some Scene {
WindowGroup {
InvoiceHomeView()
}
}
}
Note: Technically, you don't even need to declare it in the App struct, because it will be automatically initialized whenever it's first accessed, but by convention it's a good idea to keep it there.
Here's how it would be used in a view:
struct InvoiceHomeSingletonView: View {
//Access the singleton instance and assign it to a local property (for convenience)
let invoiceManager = InvoiceManager.shared
//Body
var body: some View {
NavigationStack {
VStack {
Button {
invoiceManager.create() // <- using the convenience property
InvoiceManager.shared.create() // <- this also works
} label: {
Text("Create invoice")
}
}
}
}
}
Now, whether you use one or the other is up to you, I recommend reading more on the topic. Most people, including most AI models, will say that the environment method is strongly preferred, with some going as far as calling using a singleton an anti-pattern.
Personally, I use both, because sometimes it's just easier to use a singleton and I have yet to hit any major roadblocks.
Here's a full working example using the environment method:
import SwiftUI
import SwiftData
@main
struct DemoInvoiceApp: App {
//State
@State private var invoiceManager: InvoiceManager = InvoiceManager()
var body: some Scene {
WindowGroup {
InvoiceHomeView()
.environment(invoiceManager) // <- make the InvoiceManager instance available via the environment
.modelContainer(for: Invoice.self)
}
}
}
struct InvoiceHomeView: View {
//Queries
@Query(sort: \Invoice.createdDate, order: .reverse) private var invoices: [Invoice]
//Environment
@Environment(InvoiceManager.self) private var invoiceManager
@Environment(\.modelContext) private var modelContext
//Body
var body: some View {
@Bindable var invoiceManager = invoiceManager // <- get a binding if needed
NavigationStack {
List {
if !invoices.isEmpty {
Section("Invoices") {
ForEach(invoices) {invoice in
VStack(alignment: .leading) {
LabeledContent {
Button {
invoiceManager.send(invoice: invoice)
} label: {
Image(systemName: "paperplane.fill")
}
} label: {
Text("Invoice #\(invoice.numberString)")
Text(invoice.createdDate, format: .dateTime)
}
}
}
}
}
}
.overlay {
if invoices.isEmpty {
ContentUnavailableView {
Label("No invoices", systemImage: "note.text")
} description: {
Text("Add invoices by tapping the + button.")
}
}
}
.navigationTitle("Invoice Home")
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button {
withAnimation {
invoiceManager.create()
}
} label: {
Image(systemName: "plus")
}
}
}
.safeAreaInset(edge: .bottom) {
VStack {
Toggle("Notify user on invoice creation", isOn: $invoiceManager.notifyUser ) // <- uses the binding to the instance
}
.padding(.horizontal, 30)
}
}
.task {
if invoiceManager.context == nil {
invoiceManager.context = modelContext
}
}
}
}
@Model
final class Invoice {
//Properties
var number: Int?
var createdDate: Date = Date()
var numberString: String {
if let number {
String(format: "%05d", number)
} else {
"N/A"
}
}
//Init
init(number: Int? = nil) {
self.number = number ?? Int.random(in: 0...99999)
}
}
@Observable
final class InvoiceManager {
@ObservationIgnored
var context: ModelContext?
var notifyUser: Bool = true
@MainActor
func create() {
guard let context = context else {
print("InvoiceManager context not set. Ensure the view sets it before calling create().")
return
}
//Create new invoice
let newInvoice = Invoice()
//Insert into context
context.insert(newInvoice)
//Notify user
if notifyUser {
print("-- User notified --")
}
//Save context
try? context.save()
print("Created invoice with number: \(newInvoice.number ?? -1)")
}
func send(invoice: Invoice) {
print("Send invoice function called for invoice: \(invoice.numberString)")
}
}
#Preview {
@Previewable @State var invoiceManager = InvoiceManager()
InvoiceHomeView()
.environment(invoiceManager)
.modelContainer(for: Invoice.self, inMemory: true)
}
Hope this helps.
@Observableand working with navigation, please share it.@Bindingwhich do the same thing. Normally you just have model object and view structs and swiftui handles creating/updating the actual view objects automatically. So you can think of the view structs as your view model then just delete all your unnecessary view model classes. You can init model object as a let in App because its only init once.CreateInvoiceViewModelsupposed to be specific to yourHomeScreenview or shared so that it applies to multiple views?