Fixes for multi-window support. (#5528)

* Correctly handle the re-opening of the main window.

Add an additional safe-guard to ensure only one main window exists.
Make sure all secondary windows use the correct tint colour.

* Fix a bug where the settings screen isn't shown on macOS when the AppLock feature is enabled.
This commit is contained in:
Doug
2026-05-01 14:44:04 +01:00
committed by GitHub
parent 3b0b401e49
commit 6cfcd7d41f
11 changed files with 77 additions and 17 deletions

View File

@@ -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)
}
}

View File

@@ -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)
}
}

View File

@@ -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
}
}
}

View File

@@ -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)

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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()
}

View File

@@ -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

View File

@@ -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)
}
}

View File

@@ -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
}