Shared ViewModel Across Views

TODO: collapasable, copyable, download, etc

I want to use the same ViewModel across several views. What for view model fooViewModel and views BarView, BazView, can you show me a minimal simple codebase in swift Ui that does this properly without running into Accessing StateObject's object without being installed on a View. This will create a new instance each time. runtime issues? Spatial computing goes even further with visionOS updates that bring enhanced support for volumetric apps, powerful new enterprise APIs, the new TabletopKit framework, and more.

visionOS Highlights

TabletopKit Create collaborative experiences centered around a table. This new framework handles the manipulation of cards and pieces, establishing placement and layout, and defining game boards.

New enterprise APIs provide access to spatial barcode scanning, the Apple Neural Engine, object tracking parameters, and more.

visionOS for enterprise

Volumetric APIs • Create apps that take full advantage of depth and space — and can

run side by side with other apps in the Shared Space.
• Resize volumes by using the SwiftUI scene

modifier windowResizability.

Decide if you want the user’s hands to appear in front of or behind your digital content.

Inputs

Getting started? Explore the visionOS Pathway on developer.apple.com 􀯻

• Detect planes in all orientations. • Allow anchoring objects on surfaces in your surroundings.
• Use Room Anchors to consider the user’s surroundings on a

per-room basis.
• Attach content to individual objects around the user with the new

Object Tracking API for visionOS.

Updates to scene understanding

To share a ViewModel across several views in SwiftUI without running into the issue of Accessing StateObject's object without being installed on a View, you should use the @ObservedObject and @StateObject property wrappers correctly. The @StateObject should be used in the parent view where the ViewModel is created, and the @ObservedObject should be used in the child views that share the ViewModel.

Here is a minimal example illustrating how to do this:

ViewModel

import SwiftUI
import Combine

class FooViewModel: ObservableObject {
    @Published var text: String = "Hello, world!"
}

ParentView

import SwiftUI

struct ParentView: View {
    @StateObject private var fooViewModel = FooViewModel()

    var body: some View {
        VStack {
            BarView(viewModel: fooViewModel)
            BazView(viewModel: fooViewModel)
        }
    }
}

BarView

import SwiftUI

struct BarView: View {
    @ObservedObject var viewModel: FooViewModel

    var body: some View {
        Text(viewModel.text)
    }
}

BazView

import SwiftUI

struct BazView: View {
    @ObservedObject var viewModel: FooViewModel

    var body: some View {
        TextField("Update text", text: $viewModel.text)
            .textFieldStyle(RoundedBorderTextFieldStyle())
            .padding()
    }
}

Full Content

Ensure all parts of the code are included within a Swift file:

import SwiftUI
import Combine

class FooViewModel: ObservableObject {
    @Published var text: String = "Hello, world!"
}

struct ParentView: View {
    @StateObject private var fooViewModel = FooViewModel()

    var body: some View {
        VStack {
            BarView(viewModel: fooViewModel)
            BazView(viewModel: fooViewModel)
        }
    }
}

struct BarView: View {
    @ObservedObject var viewModel: FooViewModel

    var body: some View {
        Text(viewModel.text)
    }
}

struct BazView: View {
    @ObservedObject var viewModel: FooViewModel

    var body: some View {
        TextField("Update text", text: $viewModel.text)
            .textFieldStyle(RoundedBorderTextFieldStyle())
            .padding()
    }
}

@main
struct MyApp: App {
    var body: some Scene {
        WindowGroup {
            ParentView()
        }
    }
}

Explanation:

  • FooViewModel is the shared view model class.
  • ParentView uses @StateObject to initialize the fooViewModel. This ensures that the ViewModel is created only once and managed by the SwiftUI framework.
  • BarView and BazView use @ObservedObject to observe changes in fooViewModel. Since ParentView manages the lifecycle of fooViewModel, BarView and BazView will not create new instances of fooViewModel when they access it.

This setup avoids the issue of accessing StateObject without being installed on a view, ensuring that the ViewModel instance is shared properly across multiple views.I see how ParentView composes the other views... however I'm working on a RealityKit app. It has a WindowGroup and an ImmersiveSpace. Can you do the same minimal app, but showing how to use fooViewModel across ParentView as well as ImmersiveView?In a RealityKit app that uses both a WindowGroup and an ImmersiveSpace, you can still share a ViewModel across these different views. Here's how you can adapt the previous example to work with WindowGroup and ImmersiveSpace.

ViewModel

import SwiftUI
import Combine

class FooViewModel: ObservableObject {
    @Published var text: String = "Hello, world!"
}

ParentView

import SwiftUI

struct ParentView: View {
    @ObservedObject var viewModel: FooViewModel

    var body: some View {
        VStack {
            BarView(viewModel: viewModel)
            BazView(viewModel: viewModel)
        }
    }
}

BarView

import SwiftUI

struct BarView: View {
    @ObservedObject var viewModel: FooViewModel

    var body: some View {
        Text(viewModel.text)
    }
}

BazView

import SwiftUI

struct BazView: View {
    @ObservedObject var viewModel: FooViewModel

    var body: some View {
        TextField("Update text", text: $viewModel.text)
            .textFieldStyle(RoundedBorderTextFieldStyle())
            .padding()
    }
}

ImmersiveView

import SwiftUI
import RealityKit
import RealityKitContent

struct ImmersiveView: View {
    @ObservedObject var viewModel: FooViewModel

    var body: some View {
        Text(viewModel.text)
            .background(Color.black.opacity(0.5))
            .padding()
    }
}

MyApp (Main Entry Point)

import SwiftUI

@main
struct MyApp: App {
    @StateObject private var fooViewModel = FooViewModel()

    var body: some Scene {
        WindowGroup {
            ParentView(viewModel: fooViewModel)
        }
        
        ImmersiveSpace {
            ImmersiveView(viewModel: fooViewModel)
        }
    }
}

Explanation:

  1. ViewModel: FooViewModel remains the same, managing shared state.
  2. ParentView: Uses @ObservedObject to observe the fooViewModel.
  3. BarView and BazView: Child views also use @ObservedObject to observe the shared state.
  4. ImmersiveView: A new view for the immersive space, also using @ObservedObject to observe the shared state.
  5. MyApp:
    • WindowGroup is used to create the main window with ParentView.
    • ImmersiveSpace is used to create the immersive experience with ImmersiveView.
    • Both WindowGroup and ImmersiveSpace share the same instance of FooViewModel through @StateObject.

This structure ensures that fooViewModel is shared across both the WindowGroup and the ImmersiveSpace without creating new instances, thus avoiding the runtime issue of accessing StateObject without being installed on a View.

Shared ViewModel Across Views created at Tue Jan 20 1970 21:18:11 GMT+0000 (Coordinated Universal Time)

Related Chats

Reusable SwiftUI custom view. 0.863

Immersive View Phases 0.835

SwiftUI RealityKit Integration 0.830

MultipeerManager Error Fix 0.826

Handling App Exit 0.807

Preload Assets in visionOS 0.795

MVVM: Prefix with `$` 0.784

Switch to guard statement 0.756

Animate Using withAnimation 0.749