diff --git a/ElementX/Sources/Application/Application.swift b/ElementX/Sources/Application/Application.swift index bf092174d..878664f35 100644 --- a/ElementX/Sources/Application/Application.swift +++ b/ElementX/Sources/Application/Application.swift @@ -32,7 +32,7 @@ struct Application: App { } var body: some Scene { - WindowGroup { + WindowGroup(id: SceneDelegate.mainSceneID) { appCoordinator.toPresentable() .statusBarHidden(shouldHideStatusBar) .overlay(alignment: .top) { @@ -52,7 +52,7 @@ struct Application: App { } .task { appCoordinator.start() - appCoordinator.windowManager.configure(withOpenWinddowAction: openWindow, + appCoordinator.windowManager.configure(withOpenWindowAction: openWindow, dismissWindowAction: dismissWindow) } } diff --git a/ElementX/Sources/Application/Windowing/SceneDelegate.swift b/ElementX/Sources/Application/Windowing/SceneDelegate.swift index 31b61a457..b3fd4bfa0 100644 --- a/ElementX/Sources/Application/Windowing/SceneDelegate.swift +++ b/ElementX/Sources/Application/Windowing/SceneDelegate.swift @@ -14,8 +14,18 @@ import SwiftUI class SceneDelegate: NSObject, UIWindowSceneDelegate { weak static var windowManager: SecureWindowManagerProtocol! + /// The app's main window scene identifier. + static let mainSceneID = "Main" + /// The user info key used by SwiftUI for a `WindowGroup`s `id` parameter. + static let sceneIDKey = "com.apple.SwiftUI.sceneID" + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { guard let windowScene = scene as? UIWindowScene else { return } Self.windowManager.configure(withScene: windowScene, session: session) } + + func sceneDidDisconnect(_ scene: UIScene) { + guard let windowScene = scene as? UIWindowScene else { return } + Self.windowManager.handleSceneDisconnection(windowScene) + } } diff --git a/ElementX/Sources/Application/Windowing/WindowManager.swift b/ElementX/Sources/Application/Windowing/WindowManager.swift index 692711e21..03d7b914a 100644 --- a/ElementX/Sources/Application/Windowing/WindowManager.swift +++ b/ElementX/Sources/Application/Windowing/WindowManager.swift @@ -48,15 +48,28 @@ class WindowManager: SecureWindowManagerProtocol { } func configure(withScene scene: UIWindowScene, session: UISceneSession) { - // This gets called for all opened windows, we're only interested in the - // first call, for the main window (works with state restoration too). - guard mainWindow == nil else { + // This gets called for all opened windows, we're only interested in the main window. + guard let userInfo = session.userInfo, userInfo[SceneDelegate.sceneIDKey] as? String == SceneDelegate.mainSceneID else { + scene.windows.forEach { $0.tintColor = .compound.textActionPrimary } // SecondaryWindow tinting. + return + } + + // Don't allow more than 1 main window to be presented. + if mainScene != nil { + // The window will be presented momentarily, so lets leave it blank. + scene.keyWindow?.rootViewController = UIHostingController(rootView: Color.clear) + UIApplication.shared.requestSceneSessionDestruction(session, options: nil) return } mainScene = scene mainSession = session + // Restore the previous window size on macOS as this isn't automatic. + if let previousSize = mainWindow?.frame.size { + scene.resizeWindowOnMac(to: previousSize) + } + mainWindow = scene.keyWindow mainWindow.tintColor = .compound.textActionPrimary @@ -76,12 +89,20 @@ class WindowManager: SecureWindowManagerProtocol { delegate?.windowManagerDidConfigureWindows(self) } - func configure(withOpenWinddowAction openWindowAction: OpenWindowAction, + func configure(withOpenWindowAction openWindowAction: OpenWindowAction, dismissWindowAction: DismissWindowAction) { self.openWindowAction = openWindowAction self.dismissWindowAction = dismissWindowAction } + func handleSceneDisconnection(_ scene: UIWindowScene) { + if scene == mainScene { + mainScene = nil + mainSession = nil + // Leave the mainWindow so we can reapply it's size on macOS. + } + } + func handleRoute(_ appRoute: AppRoute, windowType: SecondaryWindowType) { MXLog.info("Handling app route: \(appRoute) for window type: \(windowType)") @@ -258,3 +279,19 @@ private struct InstantlyDismissingWindow: View { } } } + +private extension UIWindowScene { + func resizeWindowOnMac(to size: CGSize) { + // Hackity hack 🔨 + guard ProcessInfo.processInfo.isiOSAppOnMac, let sizeRestrictions else { return } + + self.sizeRestrictions?.minimumSize = size + self.sizeRestrictions?.maximumSize = size + + Task { + try await Task.sleep(for: .milliseconds(100)) + self.sizeRestrictions?.minimumSize = sizeRestrictions.minimumSize + self.sizeRestrictions?.maximumSize = sizeRestrictions.maximumSize + } + } +} diff --git a/ElementX/Sources/Application/Windowing/WindowManagerProtocol.swift b/ElementX/Sources/Application/Windowing/WindowManagerProtocol.swift index 513eafe16..a5b2a314c 100644 --- a/ElementX/Sources/Application/Windowing/WindowManagerProtocol.swift +++ b/ElementX/Sources/Application/Windowing/WindowManagerProtocol.swift @@ -25,7 +25,9 @@ protocol SecureWindowManagerProtocol: WindowManagerProtocol { /// Configures the window manager to operate on the supplied scene. func configure(withScene scene: UIWindowScene, session: UISceneSession) - func configure(withOpenWinddowAction openWindowAction: OpenWindowAction, dismissWindowAction: DismissWindowAction) + func configure(withOpenWindowAction openWindowAction: OpenWindowAction, dismissWindowAction: DismissWindowAction) + + func handleSceneDisconnection(_ scene: UIWindowScene) func handleRoute(_ appRoute: AppRoute, windowType: SecondaryWindowType) diff --git a/ElementX/Sources/FlowCoordinators/SettingsFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/SettingsFlowCoordinator.swift index 6d6bfcc3f..c8ee16cb9 100644 --- a/ElementX/Sources/FlowCoordinators/SettingsFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/SettingsFlowCoordinator.swift @@ -19,6 +19,7 @@ enum SettingsFlowCoordinatorAction { class SettingsFlowCoordinator: FlowCoordinatorProtocol { private let appLockService: AppLockServiceProtocol + private let isInSecondaryWindow: Bool private let navigationStackCoordinator: NavigationStackCoordinator private let flowParameters: CommonFlowParameters @@ -39,9 +40,11 @@ class SettingsFlowCoordinator: FlowCoordinatorProtocol { } init(appLockService: AppLockServiceProtocol, + isInSecondaryWindow: Bool, navigationStackCoordinator: NavigationStackCoordinator, flowParameters: CommonFlowParameters) { self.appLockService = appLockService + self.isInSecondaryWindow = isInSecondaryWindow self.navigationStackCoordinator = navigationStackCoordinator self.flowParameters = flowParameters } @@ -72,7 +75,8 @@ class SettingsFlowCoordinator: FlowCoordinatorProtocol { private func presentSettingsScreen(animated: Bool) { let settingsScreenCoordinator = SettingsScreenCoordinator(parameters: .init(userSession: flowParameters.userSession, appSettings: flowParameters.appSettings, - isBugReportServiceEnabled: flowParameters.bugReportService.isEnabled)) + isBugReportServiceEnabled: flowParameters.bugReportService.isEnabled, + isInSecondaryWindow: isInSecondaryWindow)) settingsScreenCoordinator.actions .sink { [weak self] action in diff --git a/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift index 6c34a1854..bece7ad7e 100644 --- a/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift @@ -124,7 +124,7 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol { case .accountProvisioningLink: break // We always ignore this flow when logged in. case .settings, .chatBackupSettings: - if ProcessInfo.processInfo.isiOSAppOnMac { + if ProcessInfo.processInfo.isiOSAppOnMac, flowParameters.windowManager.secondaryWindowsEnabled { startSettingsFlow(detached: true) } else { if stateMachine.state != .settingsScreen { @@ -310,6 +310,7 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol { private func startSettingsFlow(detached: Bool) { let navigationStackCoordinator = NavigationStackCoordinator() let coordinator = SettingsFlowCoordinator(appLockService: appLockService, + isInSecondaryWindow: detached, navigationStackCoordinator: navigationStackCoordinator, flowParameters: flowParameters) diff --git a/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenCoordinator.swift b/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenCoordinator.swift index 42ece1644..57d5e935b 100644 --- a/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenCoordinator.swift +++ b/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenCoordinator.swift @@ -13,6 +13,7 @@ struct SettingsScreenCoordinatorParameters { let userSession: UserSessionProtocol let appSettings: AppSettings let isBugReportServiceEnabled: Bool + let isInSecondaryWindow: Bool } enum SettingsScreenCoordinatorAction { @@ -49,7 +50,8 @@ final class SettingsScreenCoordinator: CoordinatorProtocol { init(parameters: SettingsScreenCoordinatorParameters) { viewModel = SettingsScreenViewModel(userSession: parameters.userSession, appSettings: parameters.appSettings, - isBugReportServiceEnabled: parameters.isBugReportServiceEnabled) + isBugReportServiceEnabled: parameters.isBugReportServiceEnabled, + isInSecondaryWindow: parameters.isInSecondaryWindow) viewModel.actions .sink { [weak self] action in diff --git a/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenModels.swift b/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenModels.swift index 531b8d558..12f2a09de 100644 --- a/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenModels.swift +++ b/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenModels.swift @@ -6,8 +6,7 @@ // Please see LICENSE files in the repository root for full details. // -import Foundation -import UIKit +import SwiftUI enum SettingsScreenViewModelAction: Equatable { case close @@ -51,6 +50,8 @@ struct SettingsScreenViewState: BindableState { let isBugReportServiceEnabled: Bool + let navigationBarVisibility: Visibility + var bindings = SettingsScreenViewStateBindings() } diff --git a/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenViewModel.swift b/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenViewModel.swift index 662eff076..abeda7424 100644 --- a/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenViewModel.swift +++ b/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenViewModel.swift @@ -20,7 +20,7 @@ class SettingsScreenViewModel: SettingsScreenViewModelType, SettingsScreenViewMo actionsSubject.eraseToAnyPublisher() } - init(userSession: UserSessionProtocol, appSettings: AppSettings, isBugReportServiceEnabled: Bool) { + init(userSession: UserSessionProtocol, appSettings: AppSettings, isBugReportServiceEnabled: Bool, isInSecondaryWindow: Bool) { self.appSettings = appSettings super.init(initialViewState: .init(deviceID: userSession.clientProxy.deviceID, @@ -29,7 +29,8 @@ class SettingsScreenViewModel: SettingsScreenViewModelType, SettingsScreenViewMo showAccountDeactivation: userSession.clientProxy.canDeactivateAccount, showDeveloperOptions: appSettings.developerOptionsEnabled, showAnalyticsSettings: appSettings.canPromptForAnalytics, - isBugReportServiceEnabled: isBugReportServiceEnabled), + isBugReportServiceEnabled: isBugReportServiceEnabled, + navigationBarVisibility: isInSecondaryWindow ? .hidden : .automatic), mediaProvider: userSession.mediaProvider) appSettings.$developerOptionsEnabled diff --git a/ElementX/Sources/Screens/Settings/SettingsScreen/View/SettingsScreen.swift b/ElementX/Sources/Screens/Settings/SettingsScreen/View/SettingsScreen.swift index f3e472d1e..1767576b0 100644 --- a/ElementX/Sources/Screens/Settings/SettingsScreen/View/SettingsScreen.swift +++ b/ElementX/Sources/Screens/Settings/SettingsScreen/View/SettingsScreen.swift @@ -40,7 +40,7 @@ struct SettingsScreen: View { .compoundList() .navigationTitle(L10n.commonSettings) .navigationBarTitleDisplayMode(.inline) - .toolbarVisibility(ProcessInfo.processInfo.isiOSAppOnMac ? .hidden : .automatic, for: .navigationBar) + .toolbarVisibility(context.viewState.navigationBarVisibility, for: .navigationBar) .toolbar { toolbar } } @@ -278,6 +278,7 @@ struct SettingsScreen_Previews: PreviewProvider, TestablePreview { deviceID: "AAAAAAAAAAA")))) return SettingsScreenViewModel(userSession: userSession, appSettings: ServiceLocator.shared.settings, - isBugReportServiceEnabled: isBugReportServiceEnabled) + isBugReportServiceEnabled: isBugReportServiceEnabled, + isInSecondaryWindow: false) } } diff --git a/UnitTests/Sources/SettingsScreenViewModelTests.swift b/UnitTests/Sources/SettingsScreenViewModelTests.swift index 2322b872a..a9dff0746 100644 --- a/UnitTests/Sources/SettingsScreenViewModelTests.swift +++ b/UnitTests/Sources/SettingsScreenViewModelTests.swift @@ -19,7 +19,8 @@ struct SettingsScreenViewModelTests { let userSession = UserSessionMock(.init(clientProxy: ClientProxyMock(.init(userID: "")))) viewModel = SettingsScreenViewModel(userSession: userSession, appSettings: ServiceLocator.shared.settings, - isBugReportServiceEnabled: true) + isBugReportServiceEnabled: true, + isInSecondaryWindow: false) context = viewModel.context }