// TODO: This could possibly be renamed to ``SceneGraph`` now that that's basically the role // it has taken on since introducing scenes. /// A top-level wrapper providing an entry point for the app. Exists to be able to persist /// the view graph alongside the app (we can't do that on a user's ``App`` implementation because /// we can only add computed properties). @MainActor class _App { /// The app being run. let app: AppRoot /// An instance of the app's selected backend. let backend: AppRoot.Backend /// The root of the app's scene graph. var sceneGraphRoot: AppRoot.Body.Node? /// Cancellables for observations of the app's state properties. var cancellables: [Cancellable] /// The root level environment. var environment: EnvironmentValues /// The dynamic property updater for ``app``. var dynamicPropertyUpdater: DynamicPropertyUpdater /// Wraps a user's app implementation. init(_ app: AppRoot, backend: AppRoot.Backend) { self.backend = backend self.app = app self.environment = EnvironmentValues(backend: backend) self.cancellables = [] dynamicPropertyUpdater = DynamicPropertyUpdater(for: app) } func refreshSceneGraph() { // TODO: Do we have to update dynamic properties on state changes? // We can probably get away with only doing it when the root // environment changes. dynamicPropertyUpdater.update(app, with: environment, previousValue: nil) if let sceneGraphRoot { let result = sceneGraphRoot.updateNode(app.body, environment: environment) if let backend = backend as? any BackendFeatures.ApplicationMenus { backend.setApplicationMenu( result.preferences.commands.resolve(), environment: environment ) } sceneGraphRoot.update( backend: backend, environment: environment ) } } /// Runs the app using the app's selected backend. func run() { backend.runMainLoop { [self] in let baseEnvironment = EnvironmentValues(backend: backend) environment = backend.computeRootEnvironment( defaultEnvironment: baseEnvironment ) dynamicPropertyUpdater.update(app, with: environment, previousValue: nil) forEachField(of: app) { name, _, fieldValue in #if DEBUG if name == "state", fieldValue is ObservableObject { logger.warning( """ the App.state protocol requirement has been removed in favour of \ SwiftUI-style @State annotations; decorate \(AppRoot.self).state \ with the @State property wrapper to restore previous behaviour """ ) } #endif guard let value = fieldValue as? any ObservableProperty else { return // i.e. continue } let cancellable = value.didChange.observeAsUIUpdater(backend: backend) { [weak self] in self?.refreshSceneGraph() } cancellables.append(cancellable) } let rootNode = AppRoot.Body.Node( from: app.body, backend: backend, environment: environment ) backend.setRootEnvironmentChangeHandler { self.environment = self.backend.computeRootEnvironment( defaultEnvironment: baseEnvironment ) self.refreshSceneGraph() } let result = rootNode.updateNode(nil, environment: environment) // Update application-wide menu if let backend = backend as? any BackendFeatures.ApplicationMenus { backend.setApplicationMenu( result.preferences.commands.resolve(), environment: environment ) } rootNode.update(backend: backend, environment: environment) self.sceneGraphRoot = rootNode } } }