diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index 46766d7ab..da408584f 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -453,6 +453,7 @@ 4949C8C12669D1B5E082366E /* QRCodeLoginScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFA9EA59D5C0DA1BFC7B3621 /* QRCodeLoginScreen.swift */; }; 49500BBA1CD65A5AE252D970 /* RoomDirectorySearchScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41BB37D96C3EA18F3CE8675D /* RoomDirectorySearchScreenModels.swift */; }; 49BBEC46D523BF6A41400048 /* URLTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB34956C87731AB094DB33A /* URLTests.swift */; }; + 4A091BAF20F4D27DF66B7F0A /* PresentationColorSchemeModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDF6E8208542BE7A02A94939 /* PresentationColorSchemeModifier.swift */; }; 4A361E8BAFFA20B9719B47F5 /* InviteUsersConfirmationSheetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8E97CF050B0168F3D605F0E9 /* InviteUsersConfirmationSheetView.swift */; }; 4A4110369DBB79E4A314F415 /* ComposerToolbarViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0618820D26F9871A4BBB40E /* ComposerToolbarViewModelProtocol.swift */; }; 4A618590DEB72C4F186BFED4 /* UserSessionFlowCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C99FDEEB71173C4C6FA2734C /* UserSessionFlowCoordinator.swift */; }; @@ -3150,6 +3151,7 @@ ED49073BB1C1FC649DAC2CCD /* LocationRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationRoomTimelineView.swift; sourceTree = ""; }; ED60E4D2CD678E1EBF16F77A /* BlockedUsersScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockedUsersScreen.swift; sourceTree = ""; }; EDDE826EAB1BAB80C1104980 /* SpaceFlowCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceFlowCoordinator.swift; sourceTree = ""; }; + EDF6E8208542BE7A02A94939 /* PresentationColorSchemeModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PresentationColorSchemeModifier.swift; sourceTree = ""; }; EE378083653EF0C9B5E9D580 /* EmoteRoomTimelineItemContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmoteRoomTimelineItemContent.swift; sourceTree = ""; }; EE61568A225F7AA068583AA7 /* SignallingTimelineItemProviderMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignallingTimelineItemProviderMock.swift; sourceTree = ""; }; EE8BCD14EFED23459A43FDFF /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/Localizable.strings; sourceTree = ""; }; @@ -3466,6 +3468,7 @@ 2A38B60F40D47397CB3B3C9C /* CollapsedInPlaceModifier.swift */, F03AEAA2F66E796C365EFD58 /* ElementNavigationStack.swift */, 55BA4D3DFEA16CDD3FE99064 /* InverseMask.swift */, + EDF6E8208542BE7A02A94939 /* PresentationColorSchemeModifier.swift */, 5DB56ABD78E60920ACE43087 /* PreviewScrollView.swift */, D2C513A6CD99E6C3C163DA1E /* RowDivider.swift */, 693E16574C6F7F9FA1015A8C /* Search.swift */, @@ -9121,6 +9124,7 @@ 1307268DC41730E5BCF7D9A0 /* PollView.swift in Sources */, DF504B10A4918F971A57BEF2 /* PostHogAnalyticsClient.swift in Sources */, CF7D38F6478CDA5E48CA185D /* PresenceService.swift in Sources */, + 4A091BAF20F4D27DF66B7F0A /* PresentationColorSchemeModifier.swift in Sources */, BDCF42966007770A33D90E15 /* PreviewScrollView.swift in Sources */, 6793E75E3EBE48EBB8F857AF /* ProcessInfo.swift in Sources */, 69DE29C3E3180BB17D840690 /* ProgressCursorModifier.swift in Sources */, diff --git a/ElementX/Sources/Other/SwiftUI/PresentationColorSchemeModifier.swift b/ElementX/Sources/Other/SwiftUI/PresentationColorSchemeModifier.swift new file mode 100644 index 000000000..6c7f84e8a --- /dev/null +++ b/ElementX/Sources/Other/SwiftUI/PresentationColorSchemeModifier.swift @@ -0,0 +1,79 @@ +// +// Copyright 2026 Element Creations Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. +// Please see LICENSE files in the repository root for full details. +// + +import SwiftUI + +extension View { + /// Forces the colour scheme of the sheet or popover containing this view without touching the + /// rest of the app. Does nothing when the view isn't presented modally or the scheme is `nil`. + /// + /// Prefer this over `preferredColorScheme` for modals: SwiftUI applies that modifier to the whole + /// window and can leave it applied when the presentation is torn down abnormally (#6093). + func presentationColorScheme(_ colorScheme: ColorScheme?) -> some View { + background(PresentationColorSchemeView(colorScheme: colorScheme)) + } +} + +private struct PresentationColorSchemeView: UIViewRepresentable { + let colorScheme: ColorScheme? + + func makeUIView(context: Context) -> InterfaceStyleView { + InterfaceStyleView(style: UIUserInterfaceStyle(colorScheme)) + } + + func updateUIView(_ uiView: InterfaceStyleView, context: Context) { + uiView.style = UIUserInterfaceStyle(colorScheme) + } + + class InterfaceStyleView: UIView { + var style: UIUserInterfaceStyle { + didSet { applyStyle() } + } + + init(style: UIUserInterfaceStyle) { + self.style = style + super.init(frame: .zero) + isUserInteractionEnabled = false + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func didMoveToWindow() { + super.didMoveToWindow() + applyStyle() + } + + private func applyStyle() { + guard window != nil else { return } + targetViewController?.overrideUserInterfaceStyle = style + } + + /// The view controller that was actually presented, so the override covers its navigation bar too. + private var targetViewController: UIViewController? { + guard let next else { return nil } + return sequence(first: next) { $0.next } + .lazy + .compactMap { $0 as? UIViewController } + // Children of a presented controller also report a presentingViewController, so check the parent. + .first { $0.presentingViewController != nil && $0.parent == nil } + } + } +} + +private extension UIUserInterfaceStyle { + init(_ colorScheme: ColorScheme?) { + self = switch colorScheme { + case .light: .light + case .dark: .dark + case .none: .unspecified + @unknown default: .unspecified + } + } +} diff --git a/ElementX/Sources/Screens/FilePreviewScreen/View/TimelineMediaPreviewDetailsView.swift b/ElementX/Sources/Screens/FilePreviewScreen/View/TimelineMediaPreviewDetailsView.swift index 990cf2011..7b43e23c3 100644 --- a/ElementX/Sources/Screens/FilePreviewScreen/View/TimelineMediaPreviewDetailsView.swift +++ b/ElementX/Sources/Screens/FilePreviewScreen/View/TimelineMediaPreviewDetailsView.swift @@ -35,7 +35,7 @@ struct TimelineMediaPreviewDetailsView: View { .presentationDetents([.height(sheetHeight)]) .presentationDragIndicator(.visible) .presentationBackground(.compound.bgCanvasDefault) - .preferredColorScheme(preferredColorScheme) + .presentationColorScheme(preferredColorScheme) .sheet(item: $context.redactConfirmationItem) { item in TimelineMediaPreviewRedactConfirmationView(item: item, context: context, diff --git a/ElementX/Sources/Screens/FilePreviewScreen/View/TimelineMediaPreviewRedactConfirmationView.swift b/ElementX/Sources/Screens/FilePreviewScreen/View/TimelineMediaPreviewRedactConfirmationView.swift index 383de10c8..8d0c31c98 100644 --- a/ElementX/Sources/Screens/FilePreviewScreen/View/TimelineMediaPreviewRedactConfirmationView.swift +++ b/ElementX/Sources/Screens/FilePreviewScreen/View/TimelineMediaPreviewRedactConfirmationView.swift @@ -38,7 +38,7 @@ struct TimelineMediaPreviewRedactConfirmationView: View { .presentationDetents([.height(sheetHeight + topPadding)]) .presentationDragIndicator(.visible) .presentationBackground(.compound.bgCanvasDefault) - .preferredColorScheme(preferredColorScheme) + .presentationColorScheme(preferredColorScheme) } private var header: some View { diff --git a/ElementX/Sources/Screens/MediaUploadPreviewScreen/View/MediaUploadPreviewScreen.swift b/ElementX/Sources/Screens/MediaUploadPreviewScreen/View/MediaUploadPreviewScreen.swift index 76b8ac25f..650ad9b1a 100644 --- a/ElementX/Sources/Screens/MediaUploadPreviewScreen/View/MediaUploadPreviewScreen.swift +++ b/ElementX/Sources/Screens/MediaUploadPreviewScreen/View/MediaUploadPreviewScreen.swift @@ -16,8 +16,6 @@ import SwiftUI import UniformTypeIdentifiers struct MediaUploadPreviewScreen: View { - @Environment(\.colorScheme) private var colorScheme - @Bindable var context: MediaUploadPreviewScreenViewModel.Context @State private var captionWarningFrame: CGRect = .zero @@ -28,8 +26,10 @@ struct MediaUploadPreviewScreen: View { ProcessInfo.processInfo.isiOSAppOnMac ? context.viewState.title ?? "" : "" } - private var colorSchemeOverride: ColorScheme { - ProcessInfo.processInfo.isiOSAppOnMac ? colorScheme : .dark + /// Matches the dark chrome of the QLPreviewController. Scoped to the sheet rather than + /// using `preferredColorScheme` which leaks into the whole app if the sheet fails to present. + private var colorSchemeOverride: ColorScheme? { + ProcessInfo.processInfo.isiOSAppOnMac ? nil : .dark } var body: some View { @@ -49,7 +49,7 @@ struct MediaUploadPreviewScreen: View { .disabled(context.viewState.shouldDisableInteraction) .interactiveDismissDisabled() .presentationBackground(.background) // Fix a bug introduced by the caption warning. - .preferredColorScheme(colorSchemeOverride) + .presentationColorScheme(colorSchemeOverride) .onAppear(perform: focusComposerIfHardwareKeyboardConnected) .alert(item: $context.alertInfo) .sheet(isPresented: $context.isPresentingMediaEditor) { @@ -60,6 +60,7 @@ struct MediaUploadPreviewScreen: View { context.isPresentingMediaEditor = false } .ignoresSafeArea() + .presentationColorScheme(colorSchemeOverride) // Make sure out of bound error alerts are shown even if the sheet is presented .alert(item: $context.alertInfo) } @@ -131,7 +132,7 @@ struct MediaUploadPreviewScreen: View { .presentationDragIndicator(.visible) .padding(.top, 19) // For the drag indicator .presentationBackground(.compound.bgCanvasDefault) - .preferredColorScheme(colorSchemeOverride) + .presentationColorScheme(colorSchemeOverride) } } @@ -165,9 +166,6 @@ struct MediaUploadPreviewScreen: View { Button { context.send(viewAction: .cancel) } label: { Text(L10n.actionCancel) } - // Fix a bug with the preferredColorScheme on iOS 18 where the button doesn't - // follow the dark colour scheme on devices running with dark mode disabled. - .tint(.compound.textActionPrimary) } if isCurrentMediaImage { @@ -175,9 +173,6 @@ struct MediaUploadPreviewScreen: View { Button { context.isPresentingMediaEditor = true } label: { CompoundIcon(\.crop) } - // Fix a bug with the preferredColorScheme on iOS 18 where the button doesn't - // follow the dark colour scheme on devices running with dark mode disabled. - .tint(.compound.textActionPrimary) } } } diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/mediaUploadPreviewScreen.iPad-en-GB-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/mediaUploadPreviewScreen.iPad-en-GB-0.png index 622e2e7d0..906e3b3ad 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/mediaUploadPreviewScreen.iPad-en-GB-0.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/mediaUploadPreviewScreen.iPad-en-GB-0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:228dfc2372e109619f2627527c5124a76401ce24300112837be4b9e5d2f42964 -size 86282 +oid sha256:f327e6dfe4c81dfaa2a654b89c973293e61bc16147595ab031eb6676f8313f3e +size 86285 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/mediaUploadPreviewScreen.iPad-pseudo-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/mediaUploadPreviewScreen.iPad-pseudo-0.png index 17189dba4..10e3c9639 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/mediaUploadPreviewScreen.iPad-pseudo-0.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/mediaUploadPreviewScreen.iPad-pseudo-0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fd75072b7457c5bbbaab3876368794273590c3e83ea0ea7cc9020f57c41fdf9a -size 87335 +oid sha256:7397c1698bd8524eb1cdf99667fc1c0916b58d4fd59a752c15f6c71e89a30ae3 +size 87348