diff --git a/Example/Example.xcodeproj/project.pbxproj b/Example/Example.xcodeproj/project.pbxproj index 998067246..5f32cf2db 100644 --- a/Example/Example.xcodeproj/project.pbxproj +++ b/Example/Example.xcodeproj/project.pbxproj @@ -318,7 +318,7 @@ }; 279283B82DFF11CE00234D64 = { CreatedOnToolsVersion = 16.3; - TestTargetID = 27D49DF72BA604FB00F6E2E2; + TestTargetID = 275751E22DEE1441003E467C; }; 27CD0B482AFC8D37003665EB = { CreatedOnToolsVersion = 15.0; @@ -1116,7 +1116,7 @@ SUPPORTS_MACCATALYST = NO; SWIFT_EMIT_LOC_STRINGS = NO; TARGETED_DEVICE_FAMILY = "1,2,7"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/HostingExample.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/HostingExample"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/TestingHost.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/TestingHost"; }; name = SwiftUIDebug; }; @@ -1138,7 +1138,7 @@ SUPPORTS_MACCATALYST = NO; SWIFT_EMIT_LOC_STRINGS = NO; TARGETED_DEVICE_FAMILY = "1,2,7"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/HostingExample.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/HostingExample"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/TestingHost.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/TestingHost"; }; name = SwiftUIRelease; }; @@ -1160,7 +1160,7 @@ SUPPORTS_MACCATALYST = NO; SWIFT_EMIT_LOC_STRINGS = NO; TARGETED_DEVICE_FAMILY = "1,2,7"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/HostingExample.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/HostingExample"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/TestingHost.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/TestingHost"; }; name = OpenSwiftUIDebug; }; @@ -1182,7 +1182,7 @@ SUPPORTS_MACCATALYST = NO; SWIFT_EMIT_LOC_STRINGS = NO; TARGETED_DEVICE_FAMILY = "1,2,7"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/HostingExample.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/HostingExample"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/TestingHost.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/TestingHost"; }; name = OpenSwiftUIRelease; }; diff --git a/Sources/COpenSwiftUI/Shims/UIKit/UIKit_Private.h b/Sources/COpenSwiftUI/Shims/UIKit/UIKit_Private.h index f499d9c8f..4de279863 100644 --- a/Sources/COpenSwiftUI/Shims/UIKit/UIKit_Private.h +++ b/Sources/COpenSwiftUI/Shims/UIKit/UIKit_Private.h @@ -30,6 +30,8 @@ OPENSWIFTUI_ASSUME_NONNULL_BEGIN - (nullable NSString *)_launchTestName_openswiftui_safe_wrapper OPENSWIFTUI_SWIFT_NAME(_launchTestName()); - (void)_performBlockAfterCATransactionCommits_openswiftui_safe_wrapper:(void (^)(void))block OPENSWIFTUI_SWIFT_NAME(_performBlockAfterCATransactionCommits(_:)); + +- (void)_saveRestorationUserActivityStateForScene_openswiftui_safe_wrapper:(UIScene *)scene OPENSWIFTUI_SWIFT_NAME(_saveRestorationUserActivityState(forScene:)); @end @interface UIView (OpenSwiftUI_SPI) @@ -92,6 +94,10 @@ OPENSWIFTUI_ASSUME_NONNULL_BEGIN @end #endif +@interface UISceneConfiguration (OpenSwiftUI_SPI) +@property (nonatomic, readonly, nullable) id sceneDelegate; +@end + OPENSWIFTUI_EXPORT bool UIViewIgnoresTouchEvents(UIView *view); diff --git a/Sources/COpenSwiftUI/Shims/UIKit/UIKit_Private.m b/Sources/COpenSwiftUI/Shims/UIKit/UIKit_Private.m index e6f050bb1..5d5889e97 100644 --- a/Sources/COpenSwiftUI/Shims/UIKit/UIKit_Private.m +++ b/Sources/COpenSwiftUI/Shims/UIKit/UIKit_Private.m @@ -36,6 +36,11 @@ - (NSString *)_launchTestName_openswiftui_safe_wrapper { OPENSWIFTUI_SAFE_WRAPPER_IMP(NSString *, @"_launchTestName", nil); return func(self, selector); } + +- (void)_saveRestorationUserActivityStateForScene_openswiftui_safe_wrapper:(UIScene *)scene { + OPENSWIFTUI_SAFE_WRAPPER_IMP(void, @"_saveRestorationUserActivityStateForScene:", , UIScene *); + func(self, selector, scene); +} @end @implementation UIView (OpenSwiftUI_SPI) diff --git a/Sources/COpenSwiftUI/Shims/UserActivity/UserActivity_Private.h b/Sources/COpenSwiftUI/Shims/UserActivity/UserActivity_Private.h new file mode 100644 index 000000000..280817ed3 --- /dev/null +++ b/Sources/COpenSwiftUI/Shims/UserActivity/UserActivity_Private.h @@ -0,0 +1,27 @@ +// +// UserActivity_Private.h +// COpenSwiftUI +// +// Audited for 6.5.4 +// Status: Complete + +#ifndef UserActivity_Private_h +#define UserActivity_Private_h + +#include "OpenSwiftUIBase.h" + +#if OPENSWIFTUI_TARGET_OS_DARWIN + +#import + +OPENSWIFTUI_ASSUME_NONNULL_BEGIN + +@interface NSUserActivity (OpenSwiftUI_SPI) +@property (nonatomic, readonly) BOOL _isUniversalLink; +@end + +OPENSWIFTUI_ASSUME_NONNULL_END + +#endif /* OPENSWIFTUI_TARGET_OS_DARWIN */ + +#endif /* UserActivity_Private_h */ diff --git a/Sources/OpenSwiftUI/App/App/AppGraph.swift b/Sources/OpenSwiftUI/App/App/AppGraph.swift index 20898bdfd..1ded2c2d1 100644 --- a/Sources/OpenSwiftUI/App/App/AppGraph.swift +++ b/Sources/OpenSwiftUI/App/App/AppGraph.swift @@ -14,7 +14,8 @@ import Glibc import WASILibc #endif import OpenAttributeGraphShims -@_spi(ForOpenSwiftUIOnly) package import OpenSwiftUICore +@_spi(ForOpenSwiftUIOnly) +package import OpenSwiftUICore package final class AppGraph: GraphHost { static var shared: AppGraph? = nil { @@ -170,10 +171,55 @@ extension AppGraph { } } +// MARK: - AppGreaph + GraphDelegate + +extension AppGraph: GraphDelegate { + package func updateGraph(body: (GraphHost) -> T) -> T { + body(self) + } + + package func graphDidChange() { + data.updateSeed &+= 1 + runTransaction() + let phaseChanged = $rootScenePhase.changedValue().changed + let commandsChanged = $rootCommandsList?.changedValue().changed ?? false + // TODO: notifyObservers + _openSwiftUIUnimplementedWarning() + } + + package func preferencesDidChange() { + _openSwiftUIEmptyStub() + } +} + +extension AppGraph { + func addObserver(_ observer: some AppGraphObserver) { + observers = observers.filter { $0.base != nil } + observers.insert(HashableWeakBox(observer)) + } + + func removeObserver(_ observer: some AppGraphObserver) { + observers = observers.filter { $0.base != nil } + observers.remove(HashableWeakBox(observer)) + } + +// func notifyObservers(phaseChanged: Bool) { +// for box in observers { +// (box.base as? AppGraphObserver)?.scenesDidChange(phaseChanged: phaseChanged) +// } +// } +// +// func notifyCommandsChanged() { +// for box in observers { +// (box.base as? AppGraphObserver)?.commandsDidChange() +// } +// } +} + // MARK: - AppGraphObserver protocol AppGraphObserver: AnyObject { - func sceneDidChange(phaseChanged: Bool) + func scenesDidChange(phaseChanged: Bool) func commandsDidChange() } diff --git a/Sources/OpenSwiftUI/App/App/AppKit/AppKitAppDelegate.swift b/Sources/OpenSwiftUI/App/App/AppKit/AppKitAppDelegate.swift index 945ac59fb..d398f072c 100644 --- a/Sources/OpenSwiftUI/App/App/AppKit/AppKitAppDelegate.swift +++ b/Sources/OpenSwiftUI/App/App/AppKit/AppKitAppDelegate.swift @@ -52,6 +52,45 @@ class AppDelegate: NSResponder, NSApplicationDelegate { override func forwardingTarget(for aSelector: Selector!) -> Any? { appDelegate } + + func applicationWillFinishLaunching(_ notification: Notification) { + Update.begin() + defer { Update.end() } + // FIXME + let items = AppGraph.shared?.rootSceneList?.items ?? [] + let view = items[0].value.view + let hostingVC = NSHostingController(rootView: view) + let windowVC = WindowController(hostingVC) + windowVC.showWindow(nil) + } +} + +// FIXME: frame is zero +final class WindowController: NSWindowController where Content: View { + init(_ hostingVC: NSHostingController) { + self.hostingVC = hostingVC + super.init(window: nil) + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override var windowNibName: NSNib.Name? { "" } + + let hostingVC: NSHostingController + + override func loadWindow() { + window = NSWindow(contentRect: .init(x: 0, y: 0, width: 500, height: 300), styleMask: [.titled, .closable, .miniaturizable], backing: .buffered, defer: false) + window?.center() + } + + override func windowDidLoad() { + super.windowDidLoad() + contentViewController = hostingVC + hostingVC.host.frame = window!.frame + } } // MARK: - App Utils diff --git a/Sources/OpenSwiftUI/App/App/UIKit/UIKitAppDelegate.swift b/Sources/OpenSwiftUI/App/App/UIKit/UIKitAppDelegate.swift index 0a1bf74c6..224edb7b6 100644 --- a/Sources/OpenSwiftUI/App/App/UIKit/UIKitAppDelegate.swift +++ b/Sources/OpenSwiftUI/App/App/UIKit/UIKitAppDelegate.swift @@ -7,7 +7,9 @@ // ID: 4475FD12FD59DEBA453321BD91F6EA04 (SwiftUI) #if os(iOS) || os(visionOS) +import COpenSwiftUI import OpenAttributeGraphShims +@_spi(ForOpenSwiftUIOnly) package import OpenSwiftUICore public import UIKit #if OPENSWIFTUI_OPENCOMBINE @@ -15,11 +17,12 @@ import OpenCombine #else import Combine #endif +import OpenObservation // MARK: - AppDelegate [TODO] -class AppDelegate: UIResponder, UIApplicationDelegate { - private var fallbackDelegate: UIApplicationDelegate? +final class AppDelegate: UIResponder, UIApplicationDelegate { + fileprivate var fallbackDelegate: UIApplicationDelegate? var mainMenuController: UIKitMainMenuController? override init() { @@ -123,9 +126,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // TODO } -// MARK: - AppSceneDelegate [TODO] +// MARK: - AppSceneDelegate [Stubbed] -class AppSceneDelegate: UIResponder, UIWindowSceneDelegate { +final class AppSceneDelegate: UIResponder, UIWindowSceneDelegate { private lazy var appDelegate: AppDelegate = UIApplication.shared.delegate as! AppDelegate var window: UIWindow? private var sceneItemID: SceneID? @@ -143,6 +146,357 @@ class AppSceneDelegate: UIResponder, UIWindowSceneDelegate { super.init() } + override func responds(to aSelector: Selector!) -> Bool { + let boxResponds = (sceneDelegateBox?.delegate as? any UISceneDelegate)?.responds(to: aSelector) ?? false + let selfResponds = AppSceneDelegate.instancesRespond(to: aSelector) + return boxResponds || selfResponds + } + + override func forwardingTarget(for aSelector: Selector!) -> Any? { + guard let delegate = (sceneDelegateBox?.delegate as? any UISceneDelegate) else { + return nil + } + return delegate + } + + // MARK: - Test [Stubbed] + + // var pptTestCases: [PPTTestCase.Name] { [] } + + func runTest(_ name: String, options: [AnyHashable: Any]) { + _openSwiftUIUnimplementedWarning() + } + + // MARK: - UISceneDelegate [WIP] + + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + Update.begin() + defer { Update.end() } + guard window == nil, + let windowScene = scene as? UIWindowScene, + let appGraph = AppGraph.shared else { + return + } + if let fallbackAppDelegate = appDelegate.fallbackDelegate, + fallbackAppDelegate.responds(to: #selector(UIApplicationDelegate.application(_:configurationForConnecting:options:))), + let config = fallbackAppDelegate.application?(UIApplication.shared, configurationForConnecting: session, options: connectionOptions), + let delegateClass = config.delegateClass, + let sceneDelegate = config.sceneDelegate { + // TODO + _openSwiftUIUnimplementedWarning() + sceneDelegateBox = FallbackDelegateBox(sceneDelegate as? NSObject) + } + let restorationSceneItemID: SceneID? + let restorationData: [AnyHashable: Any] + if let activity = session.stateRestorationActivity, + let userInfo = activity.userInfo, + let sceneIDString = userInfo["org.OpenSwiftUIProject.OpenSwiftUI.sceneID"] as? String { + restorationSceneItemID = .string(sceneIDString) + restorationData = userInfo + } else { + restorationSceneItemID = nil + restorationData = [:] + } + sceneStorageValues = SceneStorageValues(restorationData) + let bridge = SceneBridge() + bridge.windowScene = windowScene + sceneBridge = bridge + var urlContexts = connectionOptions.urlContexts + let newWindow: UIWindow = Update.ensure { + makeSceneHostWindow( + restorationSceneItemID: restorationSceneItemID, + restorationData: restorationData, + connectionOptions: connectionOptions, + urlContexts: &urlContexts, + role: session.role, + windowScene: windowScene, + delegate: self + ) + } + if var userInfo = session.userInfo { + if let sceneItemID { + userInfo[sceneIDKey] = sceneItemID.description + } else { + userInfo[sceneIDKey] = nil + } + session.userInfo = userInfo + } else { + session.userInfo = nil + } + if var userInfo = session.userInfo { + if let presentationDataType { + userInfo[sceneTypeKey] = makeStableTypeData(presentationDataType).description + } else { + userInfo[sceneTypeKey] = nil + } + session.userInfo = userInfo + } else { + session.userInfo = nil + } + if let rawPresentationDataValue, + let presentationDataValue { + if var userInfo = session.userInfo { + userInfo[sceenValueKey] = rawPresentationDataValue + } else { + session.userInfo = nil + } + // TODO: SceneNavigationStrategy_Phone + // sceneItemID!.description + // TODO: SceneRequestCache + _openSwiftUIUnimplementedWarning() + } + bridge.rootViewController = newWindow.rootViewController + window = newWindow + newWindow.makeKeyAndVisible() + let item = sceneItem() + func handleConnectionOptionsCallbacks(_ storage: ConnectionOptionPayloadStorage) { + func _do(_ type: Definition.Type) where Definition: UISceneConnectionOptionDefinition { + // TODO: connectionOptions[type] + _openSwiftUIUnimplementedWarning() + } + for type in storage.types { + _do(type) + } + } + handleConnectionOptionsCallbacks(item.connectionOptionPayloadStorage) + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + handleConnectionOptionsCallbacks(item.connectionOptionPayloadStorage) + } + if let firstActivity = connectionOptions.userActivities.first { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in + guard let self else { return } + self.scene(scene, continue: firstActivity) + } + } + if let context = urlContexts.first { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in + let options = context.options + guard let self, let sceneBridge else { return } + let openURLContext = OpenURLContext( + url: context.url, + options: .init(uiSceneOpenURLOptions: options) + ) + sceneBridge.publishOpenURLContext(openURLContext) + } + } + appGraph.addPreference(CommandsKey.self) + appGraph.addObserver(self) + if let delegate = sceneDelegateBox?.delegate as? UIWindowSceneDelegate { + delegate.scene?(scene, willConnectTo: session, options: connectionOptions) + } + DispatchQueue.main.async { [windowScene] in + UIApplication.shared._saveRestorationUserActivityState(forScene: windowScene) + } + AppSceneDelegate.hasConnectedFirstScene = true + } + + private func makeSceneHostWindow( + restorationSceneItemID: SceneID?, + restorationData: [AnyHashable : Any], + connectionOptions: UIScene.ConnectionOptions, + urlContexts: inout Set, + role: UISceneSession.Role, + windowScene: UIWindowScene, + delegate: any UIHostingViewDelegate + ) -> UIWindow { + let userActivity = connectionOptions.userActivities.first + guard let appGraph = AppGraph.shared else { + preconditionFailure("Missing app graph") + } + let items = appGraph.rootSceneList?.items ?? [] + var matchedItem: SceneList.Item? + if let restorationSceneItemID { + for item in items { + if item.id == restorationSceneItemID { + matchedItem = item + break + } + } + } else if let sceneID = openWindowByIDSceneID(from: userActivity) { + _openSwiftUIUnimplementedFailure() + } else { + _openSwiftUIUnimplementedFailure() + } + // FIXME + if matchedItem == nil, let first = items.first { + matchedItem = first + } + guard let item = matchedItem else { + _openSwiftUIUnimplementedFailure() + } + self.sceneItemID = item.id + let rootView = self.makeRootView(item.value.view) + let hostingController = UIHostingController(rootView: rootView) + hostingController.host.delegate = self + hostingController.host.inheritedEnvironment = item.environment + let window = UIWindow(windowScene: windowScene) + window.rootViewController = hostingController + PlatformSceneCache.shared.addHost(hostingController, id: item.id) + return window + } + + private func openWindowByIDSceneID(from userActivity: NSUserActivity?) -> SceneID? { + _openSwiftUIUnimplementedWarning() + return nil + } + + + private static var hasConnectedFirstScene: Bool = false + + func sceneDidDisconnect(_ scene: UIScene) { + if let rootViewController = window?.rootViewController, + let sceneItemID { + PlatformSceneCache.shared.removeHost(rootViewController, id: sceneItemID) + } + forwardToFallbackSceneDelegate(selector: #selector(UISceneDelegate.sceneDidDisconnect(_:)), scene: scene) + } + + func sceneDidBecomeActive(_ scene: UIScene) { + updateScenePhase(.active, selector: #selector(UISceneDelegate.sceneDidBecomeActive(_:)), scene: scene) + } + + func sceneWillResignActive(_ scene: UIScene) { + updateScenePhase(.inactive, selector: #selector(UISceneDelegate.sceneWillResignActive(_:)), scene: scene) + } + + func sceneWillEnterForeground(_ scene: UIScene) { + updateScenePhase(.inactive, selector: #selector(UISceneDelegate.sceneWillEnterForeground(_:)), scene: scene) + } + + func sceneDidEnterBackground(_ scene: UIScene) { + updateScenePhase(.background, selector: #selector(UISceneDelegate.sceneDidEnterBackground(_:)), scene: scene) + } + + private func updateScenePhase(_ phase: ScenePhase, selector: Selector, scene: UIScene) { + scenePhase = phase + if let rootViewController = window?.rootViewController, + let sceneItemID { + scenesDidChange(phaseChanged: true) + PlatformSceneCache.shared.setPhase(phase, id: sceneItemID, host: rootViewController) + } + forwardToFallbackSceneDelegate(selector: selector, scene: scene) + } + + private func forwardToFallbackSceneDelegate(selector: Selector, scene: UIScene) { + guard let fallbackDelegate = sceneDelegateBox?.delegate as? UISceneDelegate, + fallbackDelegate.responds(to: selector) else { + return + } + _ = fallbackDelegate.perform(selector, with: scene) + } + + func scene(_ scene: UIScene, openURLContexts URLContexts: Set) { + var contexts = URLContexts + if let vc = window?.rootViewController as? DocumentBrowserViewController { + if let context = contexts.first( + where: { vc.presentDocument(at: $0.url, animated: false) } + ) { + _ = contexts.remove(context) + } + } + if let context = contexts.first, + let sceneBridge { + let openURLContext = OpenURLContext( + url: context.url, + options: .init( + uiSceneOpenURLOptions: context.options + ) + ) + sceneBridge.publishOpenURLContext(openURLContext) + } + + if let fallbackDelegate = sceneDelegateBox?.delegate as? UISceneDelegate, + fallbackDelegate.responds(to: #selector(UISceneDelegate.scene(_:openURLContexts:))) { + fallbackDelegate.scene?(scene, openURLContexts: contexts) + } + } + + func stateRestorationActivity(for scene: UIScene) -> NSUserActivity? { + let activity = NSUserActivity(activityType: stateRestorationKey) + var userInfo: [AnyHashable: Any] = [:] + if let sceneStorageValues { + userInfo.merge( + sceneStorageValues.restoredValue(), + uniquingKeysWith: { old, _ in old } + ) + } + var requiredKeys: Set = [] + if let presentationDataType { + let key = sceneTypeKey + userInfo[key] = makeStableTypeData(presentationDataType).description + requiredKeys.insert(key) + } + if let sceneItemID { + let key = sceneIDKey + userInfo[key] = sceneItemID.description + requiredKeys.insert(key) + } + if let rawPresentationDataValue { + let key = sceenValueKey + userInfo[key] = rawPresentationDataValue + requiredKeys.insert(key) + } + if !requiredKeys.isEmpty { + activity.requiredUserInfoKeys = requiredKeys + } + activity.userInfo = userInfo + return activity + } + + func scene(_ scene: UIScene, continue userActivity: NSUserActivity) { + if userActivity._isUniversalLink, + let webpageURL = userActivity.webpageURL { + let context = OpenURLContext(url: webpageURL, options: nil) + sceneBridge?.publishOpenURLContext(context) + } else { + sceneBridge?.publishActivity(userActivity) + if let sceneDelegate = sceneDelegateBox?.delegate as? UISceneDelegate, + let windowSceneDelegate = sceneDelegate as? UIWindowSceneDelegate, + windowSceneDelegate.responds(to: #selector(UISceneDelegate.scene(_:continue:))) { + windowSceneDelegate.scene?(scene, continue: userActivity) + } + } + } + + // MARK: - Scene related + + func sceneItem() -> SceneList.Item { + guard let sceneItemID, + let appGraph = AppGraph.shared + else { + preconditionFailure("Missing scene item!") + } + let items = appGraph.rootSceneList?.items ?? [] + for item in items { + guard item.id == sceneItemID else { + continue + } + return item + } + preconditionFailure("Missing scene item!") + } + + // MARK: - ConnectionOption [Stubbed] + + var connectionOptionDefinitionTarget: AnyObject? { + sceneDelegateBox?.delegate + } + + func handleConnectionOptionDefinition( + payload: Definition.Payload, + definition: Definition.Type, + scene: Definition.SceneType + ) async throws where Definition : UISceneConnectionOptionDefinition { + _openSwiftUIUnimplementedWarning() + } + + // MARK: - Document [Stubbed] + + // func makeDocumentIntroView(documentGroups: [IdentifiedDocumentGroupConfiguration], configuration: DocumentIntroductionConfiguration) -> DocumentGroupsIntroRootView + // func makeConfiguredDocumentRootViewController(_ controller: DocumentViewController) -> UIViewController + + // MARK: - Root view and modifier + private var rootModifier: RootModifier { guard let sceneBridge else { preconditionFailure("Application configuration error.") @@ -165,6 +519,66 @@ class AppSceneDelegate: UIResponder, UIWindowSceneDelegate { } } +private let sceneIDKey = "org.OpenSwiftUIProject.OpenSwiftUI.sceneID" +private let sceenValueKey = "org.OpenSwiftUIProject.OpenSwiftUI.sceneValue" +private let sceneTypeKey = "org.OpenSwiftUIProject.OpenSwiftUI.sceneType" +private let stateRestorationKey = "org.OpenSwiftUIProject.OpenSwiftUI.stateRestoration" + +// MARK: - AppSceneDelegate + UIHostingViewDelegate + +extension AppSceneDelegate: UIHostingViewDelegate { + func hostingView(_ hostingView: _UIHostingView, didMoveTo window: UIWindow?) where V: View { + _openSwiftUIEmptyStub() + } + + func hostingView(_ hostingView: _UIHostingView, willUpdate environment: inout EnvironmentValues) where V: View { + guard let mainMenuController = appDelegate.mainMenuController else { + return + } + mainMenuController.updateEnvironment(&environment) + } + + func hostingView(_ hostingView: _UIHostingView, didUpdate environment: EnvironmentValues) where V: View { + _openSwiftUIEmptyStub() + } + + func hostingView(_ hostingView: _UIHostingView, didChangePreferences preferences: PreferenceValues) where V: View { + _openSwiftUIEmptyStub() + } + + func hostingView(_ hostingView: _UIHostingView, didChangePlatformItemList itemList: PlatformItemList) where V: View { + _openSwiftUIEmptyStub() + } + + func hostingView(_ hostingView: _UIHostingView, willModifyViewInputs inputs: inout _ViewInputs) where V: View { + _openSwiftUIEmptyStub() + } +} + +// MARK: - AppSceneDelegate + AppGraphObserver [WIP] + +extension AppSceneDelegate: AppGraphObserver { + func scenesDidChange(phaseChanged: Bool) { + Update.begin() + defer { Update.end() } + let item = sceneItem() + guard phaseChanged || item.version != lastVersion else { + return + } + let view = item.value.view + if let window, + let rootVC = window.rootViewController as? UIHostingController> { + rootVC.rootView = makeRootView(view) + rootVC.host.inheritedEnvironment = item.environment + } + _openSwiftUIUnimplementedWarning() + } + + func commandsDidChange() { + _openSwiftUIEmptyStub() + } +} + // MARK: - RootModifier struct RootModifier: ViewModifier { diff --git a/Sources/OpenSwiftUI/App/App/UIKit/UIKitMainMenuController.swift b/Sources/OpenSwiftUI/App/App/UIKit/UIKitMainMenuController.swift index 4ede13213..b5d8a6a7e 100644 --- a/Sources/OpenSwiftUI/App/App/UIKit/UIKitMainMenuController.swift +++ b/Sources/OpenSwiftUI/App/App/UIKit/UIKitMainMenuController.swift @@ -1,10 +1,14 @@ // // UIKitMainMenuController.swift // OpenSwiftUI +// +// Audited for 6.5.4 +// Status: WIP +// ID: B619265B3CBBC7F42E2392FC185432F2 (SwiftUI) #if os(iOS) || os(visionOS) - import UIKit + // TODO class UIKitMainMenuController { func buildMenu(with: any UIMenuBuilder) { @@ -23,6 +27,15 @@ class UIKitMainMenuController { func _performMainMenuShortcutKeyCommand(_ keyCommand: UIKeyCommand) { _openSwiftUIUnimplementedWarning() } -} + private func documentCommands() -> PlatformItemList { + _openSwiftUIUnimplementedWarning() + return PlatformItemList(items: []) + } + + @inline(__always) + func updateEnvironment(_ env: inout EnvironmentValues) { + env.documentCommands = documentCommands() + } +} #endif diff --git a/Sources/OpenSwiftUI/App/Document/UIKit/DocumentBrowserViewController.swift b/Sources/OpenSwiftUI/App/Document/UIKit/DocumentBrowserViewController.swift new file mode 100644 index 000000000..8c883b431 --- /dev/null +++ b/Sources/OpenSwiftUI/App/Document/UIKit/DocumentBrowserViewController.swift @@ -0,0 +1,21 @@ +// +// DocumentBrowserViewController.swift +// OpenSwiftUI +// +// Status: Empty-Stubbed + +#if os(iOS) || os(visionOS) + +import UIKit + +// MARK: - DocumentBrowserViewController [WIP] + +// TODO +class DocumentBrowserViewController: UIDocumentBrowserViewController { + func presentDocument(at url: URL, animated: Bool) -> Bool { + // TODO + _openSwiftUIUnimplementedWarning() + return false + } +} +#endif diff --git a/Sources/OpenSwiftUI/App/Document/UIKit/DocumentKey.swift b/Sources/OpenSwiftUI/App/Document/UIKit/DocumentKey.swift new file mode 100644 index 000000000..f0e894bb9 --- /dev/null +++ b/Sources/OpenSwiftUI/App/Document/UIKit/DocumentKey.swift @@ -0,0 +1,22 @@ +// +// DocumentKey.swift +// OpenSwiftUI +// +// Audited for 6.5.4 +// Status: WIP +// ID: 0F42DDF44729C152DA9EC9F6F4D00118 (SwiftUI?) + +#if os(iOS) || os(visionOS) +import OpenSwiftUICore + +extension EnvironmentValues { + private struct DocumentCommandsKey: EnvironmentKey { + static var defaultValue: PlatformItemList { .init(items: []) } + } + + var documentCommands: PlatformItemList { + get { self[DocumentCommandsKey.self] } + set { self[DocumentCommandsKey.self] = newValue } + } +} +#endif diff --git a/Sources/OpenSwiftUI/App/Scene/PlatformSceneCache.swift b/Sources/OpenSwiftUI/App/Scene/PlatformSceneCache.swift new file mode 100644 index 000000000..802c0e25a --- /dev/null +++ b/Sources/OpenSwiftUI/App/Scene/PlatformSceneCache.swift @@ -0,0 +1,79 @@ +// +// PlatformSceneCache.swift +// OpenSwiftUI +// +// Audited for 6.5.4 +// Status: Complete + +import OpenSwiftUICore + +// MARK: - PlatformSceneCache + +final class PlatformSceneCache { + static let shared = PlatformSceneCache() + + struct Info { + var scenes: [HashableWeakBox: ScenePhase] + } + + var infoMap: [SceneID: Info] = [:] + + private init() { + _openSwiftUIEmptyStub() + } + + func addHost(_ host: PlatformViewController, id: SceneID) { + let box = HashableWeakBox(host) + guard infoMap[id]?.scenes[box] == nil else { return } + if var info = infoMap[id] { + info.scenes[box] = .active + infoMap[id] = info + } else { + infoMap[id] = Info(scenes: [box: .active]) + } + } + + func removeHost(_ host: PlatformViewController, id: SceneID) { + guard var info = infoMap[id] else { return } + let box = HashableWeakBox(host) + info.scenes[box] = nil + if info.scenes.isEmpty { + infoMap[id] = nil + } else { + infoMap[id] = info + } + } + + func setPhase(_ phase: ScenePhase, id: SceneID, host: PlatformViewController) { + guard var info = infoMap[id] else { return } + let box = HashableWeakBox(host) + info.scenes[box] = phase + infoMap[id] = info + guard let appGraph = AppGraph.shared else { return } + Update.ensure { [self] in + let items = appGraph.rootSceneList?.items ?? [] + let phases: [ScenePhase] = items.compactMap { item -> ScenePhase? in + guard let scenes = infoMap[item.id]?.scenes else { + return nil + } + if isLinkedOnOrAfter(.v6) { + return scenes.values.max() ?? .background + } else { + return scenes.values.min() ?? .background + } + } + let finalPhase: ScenePhase + if phases.isEmpty { + finalPhase = .background + } else if isLinkedOnOrAfter(.v6) { + finalPhase = phases.max() ?? .background + } else { + finalPhase = phases.min() ?? .background + } + let changed = appGraph.$rootScenePhase.setValue(finalPhase) + if changed { + appGraph.graphDidChange() + } + } + } +} diff --git a/Sources/OpenSwiftUI/App/Scene/SceneBridge.swift b/Sources/OpenSwiftUI/App/Scene/SceneBridge.swift index 83f07049c..3df50f790 100644 --- a/Sources/OpenSwiftUI/App/Scene/SceneBridge.swift +++ b/Sources/OpenSwiftUI/App/Scene/SceneBridge.swift @@ -111,7 +111,15 @@ final class SceneBridge: ObservableObject, CustomStringConvertible { var isAnimatingSceneResize: Bool = false #if os(iOS) || os(visionOS) weak var windowScene: UIWindowScene? - weak var rootViewController: UIViewController? + weak var rootViewController: UIViewController? { + didSet { + if let initialUserActivity, + let rootViewController { + rootViewController.userActivity = initialUserActivity + initialUserActivity.becomeCurrent() + } + } + } private var sceneDefinitionOptionsSeedTracker: VersionSeedTracker = .init() var sceneDefinitionOptions: ConnectionOptionPayloadStorage = .init() private var titleSeedTracker: VersionSeedTracker = .init() @@ -124,7 +132,7 @@ final class SceneBridge: ObservableObject, CustomStringConvertible { private var sceneActivationConditions: (preferring: Set, allowing: Set)? fileprivate var userActivityTrackingInfo: UserActivityTrackingInfo? { didSet { - _ = publishEvent( + publishEvent( event: userActivityTrackingInfo as Any, type: UserActivityTrackingInfo?.self, identifier: "UserActivityTrackingInfo" @@ -177,6 +185,7 @@ final class SceneBridge: ObservableObject, CustomStringConvertible { // MARK: - Event Publishing + @discardableResult fileprivate func publishEvent(event: Any, type: Any.Type, identifier: String) -> Bool { guard Self._devNullSceneBridge == nil || Self._devNullSceneBridge !== self, let publishers = sceneBridgePublishers[AnyHashable(ObjectIdentifier(type))], @@ -197,7 +206,7 @@ final class SceneBridge: ObservableObject, CustomStringConvertible { } enqueuedEvents.removeValue(forKey: identifier) for event in events { - _ = publishEvent(event: event, type: type, identifier: identifier) + publishEvent(event: event, type: type, identifier: identifier) } } @@ -900,4 +909,26 @@ struct OpenURLContext { var options: OpenURLOptions? #endif } + +extension SceneBridge { + @inline(__always) + @discardableResult + func publishActivity(_ activity: NSUserActivity) -> Bool { + publishEvent( + event: activity, + type: NSUserActivity.self, + identifier: activity.activityType + ) + } + + @inline(__always) + @discardableResult + func publishOpenURLContext(_ context: OpenURLContext) -> Bool { + publishEvent( + event: context, + type: OpenURLContext.self, + identifier: "OpenURLContext" + ) + } +} #endif diff --git a/Sources/OpenSwiftUI/App/Scene/SceneID.swift b/Sources/OpenSwiftUI/App/Scene/SceneID.swift index a061b3146..ebbc7ed4b 100644 --- a/Sources/OpenSwiftUI/App/Scene/SceneID.swift +++ b/Sources/OpenSwiftUI/App/Scene/SceneID.swift @@ -14,16 +14,24 @@ enum SceneID: Hashable { case string(String) case type(Any.Type, UInt8) + @inline(__always) + var description: String { + switch self { + case let .string(string): + return string + case let .type(type, value): + return "\(_typeName(type))-\(value)" + } + } + static func == (lhs: SceneID, rhs: SceneID) -> Bool { switch (lhs, rhs) { case let (.string(lhsString), .string(rhsString)): return lhsString == rhsString case let (.type(lhsType, lhsValue), .type(rhsType, rhsValue)): return lhsType == rhsType && lhsValue == rhsValue - case let (.string(lhsString), .type(rhsType, rhsValue)): - return lhsString == "\(_typeName(rhsType))-\(rhsValue)" - case let (.type(lhsType, lhsValue), .string(rhsString)): - return "\(_typeName(lhsType))-\(lhsValue)" == rhsString + default: + return lhs.description == rhs.description } } diff --git a/Sources/OpenSwiftUI/App/Scene/SceneList.swift b/Sources/OpenSwiftUI/App/Scene/SceneList.swift index c455e4b87..6d53e618d 100644 --- a/Sources/OpenSwiftUI/App/Scene/SceneList.swift +++ b/Sources/OpenSwiftUI/App/Scene/SceneList.swift @@ -72,6 +72,19 @@ extension SceneList { case singleWindow(SingleWindowConfiguration) // case documentIntroduction(DocumentIntroductionConfiguration) // case alertDialog(DialogConfiguration) + + @inline(__always) + var view: AnyView { + switch self { + case let .windowGroup(config): + return config.mainContent + case let .settings(view): + return view + case .singleWindow: + // TODO: Implement SingleWindowConfiguration.view + return AnyView(EmptyView()) + } + } } // MARK: - SceneList.Item.Kind diff --git a/Sources/OpenSwiftUI/App/Scene/SceneStorage.swift b/Sources/OpenSwiftUI/App/Scene/SceneStorage.swift index 048aea65b..917d6835f 100644 --- a/Sources/OpenSwiftUI/App/Scene/SceneStorage.swift +++ b/Sources/OpenSwiftUI/App/Scene/SceneStorage.swift @@ -10,7 +10,25 @@ import OpenSwiftUICore // MARK: - SceneStorageValues [WIP] -class SceneStorageValues {} +class SceneStorageValues { + private var encodedValues: [AnyHashable: Any] + private var locations: [String: AnyEntry] = [:] + var encodedValueCount: Int = 0 + weak var associatedHost: ViewRendererHost? = nil + + init(_ value: [AnyHashable: Any]) { + encodedValues = value + } + + func restoredValue() -> [AnyHashable: Any] { + _openSwiftUIUnimplementedWarning() + return [:] + } + + private class AnyEntry {} + + private class Entry {} +} // MARK: - EnvironmentValues + sceneStorageValues diff --git a/Sources/OpenSwiftUI/Integration/Hosting/UIKit/View/UIHostingView.swift b/Sources/OpenSwiftUI/Integration/Hosting/UIKit/View/UIHostingView.swift index b9b356fc9..cf98671db 100644 --- a/Sources/OpenSwiftUI/Integration/Hosting/UIKit/View/UIHostingView.swift +++ b/Sources/OpenSwiftUI/Integration/Hosting/UIKit/View/UIHostingView.swift @@ -576,12 +576,12 @@ extension _UIHostingView { // MARK: - UIHostingViewDelegate package protocol UIHostingViewDelegate: AnyObject { - func hostingView(_ hostingView: _UIHostingView, didMoveTo window: UIWindow?) where A: View - func hostingView(_ hostingView: _UIHostingView, willUpdate environment: inout EnvironmentValues) where A: View - func hostingView(_ hostingView: _UIHostingView, didUpdate environment: EnvironmentValues) where A: View - func hostingView(_ hostingView: _UIHostingView, didChangePreferences preferences: PreferenceValues) where A: View - func hostingView(_ hostingView: _UIHostingView, didChangePlatformItemList itemList: PlatformItemList) where A: View - func hostingView(_ hostingView: _UIHostingView, willModifyViewInputs inputs: inout _ViewInputs) where A: View + func hostingView(_ hostingView: _UIHostingView, didMoveTo window: UIWindow?) where V: View + func hostingView(_ hostingView: _UIHostingView, willUpdate environment: inout EnvironmentValues) where V: View + func hostingView(_ hostingView: _UIHostingView, didUpdate environment: EnvironmentValues) where V: View + func hostingView(_ hostingView: _UIHostingView, didChangePreferences preferences: PreferenceValues) where V: View + func hostingView(_ hostingView: _UIHostingView, didChangePlatformItemList itemList: PlatformItemList) where V: View + func hostingView(_ hostingView: _UIHostingView, willModifyViewInputs inputs: inout _ViewInputs) where V: View } #endif diff --git a/Sources/OpenSwiftUI/Integration/PlatformItemList.swift b/Sources/OpenSwiftUI/Integration/PlatformItemList.swift index ff8aa278b..d6b7b33ea 100644 --- a/Sources/OpenSwiftUI/Integration/PlatformItemList.swift +++ b/Sources/OpenSwiftUI/Integration/PlatformItemList.swift @@ -7,16 +7,16 @@ // FIXME package struct PlatformItemList { - var itesms: [Item] + var items: [Item] // FIXME struct Item {} fileprivate struct Key: PreferenceKey { - static let defaultValue: PlatformItemList = .init(itesms: []) + static let defaultValue: PlatformItemList = .init(items: []) static func reduce(value: inout PlatformItemList, nextValue: () -> PlatformItemList) { - value.itesms.append(contentsOf: nextValue().itesms) + value.items.append(contentsOf: nextValue().items) } } } diff --git a/Sources/OpenSwiftUICore/Data/Util/Box.swift b/Sources/OpenSwiftUICore/Data/Util/Box.swift index d991b7627..6b5bc2b65 100644 --- a/Sources/OpenSwiftUICore/Data/Util/Box.swift +++ b/Sources/OpenSwiftUICore/Data/Util/Box.swift @@ -81,13 +81,13 @@ package struct WeakBox where T: AnyObject { // MARK: - HashableWeakBox -package struct HashableWeakBox: Hashable where T: AnyObject{ +package struct HashableWeakBox: Hashable where T: AnyObject { weak package var base: T? let basePointer: UnsafeMutableRawPointer @inlinable - init(_ base: T) { + package init(_ base: T) { self.base = base self.basePointer = Unmanaged.passUnretained(base).toOpaque() }