SwiftData Loading Strategy
The SwiftData ModelContainer init function may throw and Error , the problem with that is that iOS Apps working with SwiftData can not afford to work if their SwiftData model wasn't initiated and most of the time we may need initiate this model at loading time. On top of that, is very common to have the App data updated with data dowloaded (obtained) from the network and because of that, most of the time the right place to hold the Model Container is within the Class that manages the network calls.
Solution
The main idea is to have a group that shows a temporary (loading) View until everything needed for the normal run of the app is loaded/inited and when this job is done then present the main View, commonly called ContentView.
The example below calls to ModelContainerFactory().makeOne(), the nice of this function is that create the SwiftData.ModelContainer in background so during this time the UI is user reactive. On top of that also throws an error.
The class AppManager, in general, will be the one holding global parameters, making the network calls (through others SwiftUI.actors ) and updating the Data hosted in the device. The class AppManager may hold too a class that I usually call SSOModel which is the that contains the user info which can be saved in the UserDefault space and in the Keychain space for example if is required to save User's tokens. When there is a SSOModel, the one I like to use it is the ContentView View where it decides to present the Sign Up/Login View or the Main View App.
@main
struct OneRepMaxApp: App {
@State private var isLoading = true
private var appManager: AppManager = .init()
var body: some Scene {
WindowGroup {
Group {
if isLoading == false,
let modelContainer = appManager.modelContainer {
ContentView()
.environment(appManager)
.modelContainer(modelContainer)
} else {
//Instead of the text below it should be a real
// meaningful view
Text("Loading Model Data")
}
}
.task {
do {
appManager
.modelContainer = try await ModelContainerFactory()
.makeOne()
isLoading = false
} catch {
// We can use OSLog to log infor to the terminal
logger.critical("Failure loading model")
// We can do something here, eg. to present an alert
// view and request to close the app.
}
}
}
}
}
actor ModelContainerFactory {
init() {}
func makeOne(isStoredInMemoryOnly: Bool = false) throws
-> ModelContainer {
let schema = Schema([
Workout.self,
WorkoutHistorical.self,
])
let modelConfiguration =
ModelConfiguration(schema: schema,
isStoredInMemoryOnly: isStoredInMemoryOnly)
return try ModelContainer(for: schema,
configurations: [modelConfiguration])
}
}
Conclusion
This SwiftData loading strategy offers several key benefits:
- Robust Initialization: By handling potential errors during ModelContainer creation, the app becomes more resilient to initialization failures.
- Improved User Experience: The loading view provides visual feedback to users while critical app components are being initialized.
- Background Processing: Creating the ModelContainer in a background task keeps the UI responsive during app launch.
- Flexibility: The AppManager class allows for centralized management of global parameters and network operations.
- Scalability: This approach can easily accommodate additional initialization tasks as your app grows in complexity.
Potential use cases for this strategy include:
- Apps with complex data models that require careful initialization
- Applications that need to perform network operations or data loading or migrations during launch
- Multi-user apps that require different initialization paths based on user authentication status
- Apps that need to ensure critical components are fully loaded before allowing user interaction
By implementing this strategy, developers can create more stable, responsive, and user-friendly SwiftUI apps that leverage SwiftData effectively. This approach sets a solid foundation for building sophisticated applications that can gracefully handle the challenges of data management and app initialization.