When developing multi-platform apps for macOS, iOS, and iPadOS using SwiftData, you might have noticed something curious about Xcode's default templates. They provide a starting point, sure, but when it comes to error handling, they leave much to be desired. This oversight can lead to unexpected crashes and poor user experience - issues that no developer wants to face in production.
The Default Template: A Crash Course
Here's what Xcode gives us out of the box:
@main
struct InspirioApp: App {
var sharedModelContainer: ModelContainer = {
let schema = Schema([
Item,
])
let modelConfiguration = ModelConfiguration(schema: schema,
isStoredInMemoryOnly: false)
do {
return try ModelContainer(for: schema,
configurations: [modelConfiguration])
} catch {
fatalError("Could not create ModelContainer: \(error)")
}
}()
var body: some Scene {
WindowGroup {
DashboardView()
}
.modelContainer(sharedModelContainer)
}
}
Notice that fatalError()
call? It's like telling your app, "If something goes wrong, just give up and crash." Not exactly the user experience we're aiming for, is it?
A More Robust Approach
Instead of throwing in the towel at the first sign of trouble, let's build a template that gracefully handles errors and gives users a chance to recover. Here's my proposed improvement:
@main
struct InspirioApp: App {
@State private var modelContainer: ModelContainer?
@State private var errorMessage: String?
var body: some Scene {
WindowGroup {
Group {
if let container = modelContainer {
DashboardView()
.modelContainer(container)
} else if let error = errorMessage {
ErrorView(message: error, retryAction: createModelContainer)
} else {
ProgressView("Loading...")
.onAppear(perform: createModelContainer)
}
}
}
}
private func createModelContainer() {
let schema = Schema([
Item,
])
let modelConfiguration = ModelConfiguration(schema: schema,
isStoredInMemoryOnly: false)
do {
self.modelContainer = try ModelContainer(for: schema,
configurations: [modelConfiguration])
} catch {
self.errorMessage = "Failed to create model container: \(error.localizedDescription)"
}
}
}
// ErrorView.swift
struct ErrorView: View {
let message: String
let retryAction: () -> Void
var body: some View {
VStack(spacing: 20) {
Image(systemName: "exclamationmark.triangle")
.resizable()
.scaledToFit()
.frame(width: 60, height: 60)
.foregroundColor(.red)
Text("An Error Occurred")
.font(.title)
.fontWeight(.bold)
Text(message)
.font(.body)
.multilineTextAlignment(.center)
.padding(.horizontal)
.foregroundColor(.secondary)
Button(action: retryAction) {
Text("Try Again")
.fontWeight(.semibold)
.padding()
.frame(maxWidth: .infinity)
.cornerRadius(10)
}
.padding(.horizontal, 40)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.edgesIgnoringSafeArea(.all)
}
}
Why This Approach is Better
-
Graceful Error Handling: Instead of crashing the app, we're displaying an error message to the user. This is crucial for maintaining a positive user experience, especially when the error might be temporary or fixable.
-
Retry Functionality: We've added a "Try Again" button, allowing users to attempt to recreate the model container. This is particularly useful for transient errors that might resolve on a second attempt.
-
Improved User Feedback: The loading state with a progress view informs the user that something is happening, rather than leaving them with a blank screen.
-
Separation of Concerns: By moving the model container creation into a separate function, we've made the code more modular and easier to maintain.
-
State Management: Using
@State
properties for the model container and error message allows for more dynamic and reactive UI updates. -
Flexibility: This structure makes it easier to add more sophisticated error handling or recovery mechanisms in the future.
-
Better Debugging: Instead of just crashing, this approach gives developers more information about what went wrong, making it easier to diagnose and fix issues.
Here is a preview of the default styling of the error view:



Conclusion
While Xcode's default template provides a starting point, as developers, it's our responsibility to create robust, user-friendly applications. By implementing better error handling from the get-go, we're setting ourselves up for success and providing a smoother experience for our users.
Remember, the goal isn't just to build an app that works when everything goes right, but one that gracefully handles the unexpected. Happy coding!