- Published on
> Working with Optional ObservableObject in SwiftUI
- Authors

- Name
- Mick MacCallum
- @0x7fs
SwiftUI's @ObservedObject and @StateObject property wrappers don't support optional types directly. You can't simply declare @ObservedObject var viewModel: MyViewModel? and have SwiftUI handle nil values gracefully. This limitation comes up when a view might not always have an associated object, like a detail view that shows content for a selected item, or when data loads asynchronously and might not be available yet.
The Core Issue
If you try to use an optional with these property wrappers, you'll hit compiler errors:
class ItemViewModel: ObservableObject {
@Published var title: String = ""
}
struct DetailView: View {
// This won't work
@ObservedObject var viewModel: ItemViewModel?
var body: some View {
Text(viewModel?.title ?? "No item selected")
}
}
The property wrappers expect a non-optional ObservableObject. So how do you handle cases where the object legitimately might not exist?
Approach 1: Conditional View Creation
The simplest solution is to only create the view when you have a valid object. Handle the optional at the parent level:
struct ContentView: View {
@State private var selectedItem: Item?
var body: some View {
NavigationSplitView {
ItemList(selection: $selectedItem)
} detail: {
if let item = selectedItem {
DetailView(viewModel: ItemViewModel(item: item))
} else {
Text("Select an item")
.foregroundColor(.secondary)
}
}
}
}
struct DetailView: View {
@ObservedObject var viewModel: ItemViewModel
var body: some View {
Text(viewModel.title)
}
}
This pattern keeps the child view simple and pushes the nil-checking to the parent. The child view always receives a valid object.
Approach 2: Wrapper View Pattern
Sometimes you want to encapsulate the optional handling within a single view. You can create a wrapper that handles both states:
struct OptionalDetailView: View {
let item: Item?
var body: some View {
if let item = item {
DetailViewContent(viewModel: ItemViewModel(item: item))
} else {
EmptyStateView()
}
}
}
private struct DetailViewContent: View {
@StateObject var viewModel: ItemViewModel
var body: some View {
VStack {
Text(viewModel.title)
Text(viewModel.description)
}
}
}
private struct EmptyStateView: View {
var body: some View {
ContentUnavailableView(
"No Selection",
systemImage: "doc.text",
description: Text("Select an item to view its details")
)
}
}
The outer view handles the optional, while the inner view works with a guaranteed non-nil object.
Approach 3: Environment Object with Optional Binding
For objects that should be accessible throughout a view hierarchy but might not always exist, consider an environment approach:
class SessionManager: ObservableObject {
@Published var currentUser: User?
}
struct ProfileButton: View {
@EnvironmentObject var session: SessionManager
var body: some View {
if let user = session.currentUser {
UserProfileView(user: user)
} else {
SignInButton()
}
}
}
The SessionManager itself is always present, but its properties can be optional. Views observe changes to the entire object, including when optional properties go from nil to a value or vice versa.
Approach 4: View Model with Loading State
Another pattern represents the optional state explicitly within your view model:
class DataViewModel: ObservableObject {
enum LoadState {
case idle
case loading
case loaded(ItemData)
case failed(Error)
}
@Published var state: LoadState = .idle
func load() async {
state = .loading
do {
let data = try await fetchData()
state = .loaded(data)
} catch {
state = .failed(error)
}
}
}
struct DataView: View {
@StateObject private var viewModel = DataViewModel()
var body: some View {
Group {
switch viewModel.state {
case .idle:
Color.clear.onAppear { Task { await viewModel.load() } }
case .loading:
ProgressView()
case .loaded(let data):
ContentView(data: data)
case .failed(let error):
ErrorView(error: error)
}
}
}
}
This approach gives you fine-grained control over all possible states without needing an optional ObservableObject.
Approach 5: Generic Optional Wrapper
For reusable code, you can create a generic wrapper that handles optional observation:
struct OptionalObservedView<Object: ObservableObject, Content: View, Placeholder: View>: View {
let object: Object?
let content: (Object) -> Content
let placeholder: () -> Placeholder
init(
_ object: Object?,
@ViewBuilder content: @escaping (Object) -> Content,
@ViewBuilder placeholder: @escaping () -> Placeholder
) {
self.object = object
self.content = content
self.placeholder = placeholder
}
var body: some View {
if let object = object {
ObservingView(object: object, content: content)
} else {
placeholder()
}
}
}
private struct ObservingView<Object: ObservableObject, Content: View>: View {
@ObservedObject var object: Object
let content: (Object) -> Content
var body: some View {
content(object)
}
}
// Usage
struct MyView: View {
let viewModel: ItemViewModel?
var body: some View {
OptionalObservedView(viewModel) { vm in
Text(vm.title)
} placeholder: {
Text("Nothing to show")
}
}
}
This encapsulates the observation mechanics while providing a clean API for optional objects.
When to Use Each Approach
The conditional view creation approach works best for simple cases where a parent view manages selection state. The wrapper pattern suits situations where you want to keep optional handling localized. Environment objects work well for app-wide optional state like user sessions. The loading state pattern is ideal for async data that goes through multiple states. The generic wrapper provides the most reusability if you face this pattern frequently.
Whichever approach you choose, the key is to unwrap the optional before passing it to a view that uses @ObservedObject or @StateObject. These property wrappers need a concrete object to observe, so handle the nil case at a level where you can provide appropriate fallback UI.
// Continue_Learning
@State vs @Binding in SwiftUI: When to Use Each
Understand the difference between @State and @Binding in SwiftUI, when to use each, and how they work together to manage data flow between views.
Detecting When App Enters Background and Saving State in SwiftUI
Learn how to detect app lifecycle changes in SwiftUI, save state when your app enters the background, and restore it when users return, using scenePhase and UIApplication notifications.
Implementing Undo/Redo in SwiftUI with UndoManager
Learn how to implement undo/redo functionality in SwiftUI using UndoManager, including environment injection, registering actions, and creating undo buttons for professional editing experiences.
// Stay Updated
Get notified when I publish new tutorials on Swift, SwiftUI, and iOS development. No spam, unsubscribe anytime.