Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor FXIOS-9331 [Theming] Fix ThemeManager issues #20667

Merged
merged 11 commits into from
Jun 17, 2024
174 changes: 60 additions & 114 deletions BrowserKit/Sources/Common/Theming/DefaultThemeManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ public final class DefaultThemeManager: ThemeManager, Notifiable {

// MARK: - Variables

private var windowThemeState: [WindowUUID: Theme] = [:]
private var windows: [WindowUUID: UIWindow] = [:]
private var allWindowUUIDs: [WindowUUID] { return Array(windows.keys) }
public var notificationCenter: NotificationProtocol
Expand All @@ -44,10 +43,6 @@ public final class DefaultThemeManager: ThemeManager, Notifiable {
return userDefaults.bool(forKey: ThemeKeys.NightMode.isOn)
}

private func privateModeIsOn(for window: WindowUUID) -> Bool {
return getPrivateThemeIsOn(for: window)
}

public var systemThemeIsOn: Bool {
return userDefaults.bool(forKey: ThemeKeys.systemThemeIsOn)
}
Expand Down Expand Up @@ -83,132 +78,102 @@ public final class DefaultThemeManager: ThemeManager, Notifiable {
UIApplication.didBecomeActiveNotification])
}

// MARK: - ThemeManager

public func windowNonspecificTheme() -> Theme {
switch getNormalSavedTheme() {
case .dark, .nightMode, .privateMode: return DarkTheme()
case .light: return LightTheme()
}
}

public func windowDidClose(uuid: WindowUUID) {
windows.removeValue(forKey: uuid)
windowThemeState.removeValue(forKey: uuid)
}

public func setWindow(_ window: UIWindow, for uuid: WindowUUID) {
windows[uuid] = window
updateSavedTheme(to: getNormalSavedTheme())
updateCurrentTheme(to: fetchSavedThemeType(for: uuid), for: uuid)
}

public func currentTheme(for window: WindowUUID?) -> Theme {
// MARK: - Themeing general functions
public func getCurrentTheme(for window: WindowUUID?) -> Theme {
guard let window else {
assertionFailure("Attempt to get the theme for a nil window UUID.")
return DarkTheme()
}

return windowThemeState[window] ?? DarkTheme()
return getThemeFrom(type: determineThemeType(for: window))
}

public func changeCurrentTheme(_ newTheme: ThemeType, for window: WindowUUID) {
guard currentTheme(for: window).type != newTheme else { return }

updateSavedTheme(to: newTheme)

// Although we may have only explicitly changed the state on one specific window,
// we want to be sure we update all windows in case the Light/Dark theme changed.
public func applyThemeUpdatesToWindows() {
allWindowUUIDs.forEach {
updateCurrentTheme(to: fetchSavedThemeType(for: $0), for: $0, notify: false)
applyThemeChanges(for: $0, using: determineThemeType(for: $0))
}

// After updating all windows, notify (once). We send the UUID of the window for
// which the change originated though more than 1 window may ultimately update its UI.
notifyCurrentThemeDidChange(for: window)
}

public func reloadTheme(for window: WindowUUID) {
updateCurrentTheme(to: fetchSavedThemeType(for: window), for: window)
// MARK: - Manual theme functions
public func setManualTheme(to newTheme: ThemeType) {
updateSavedTheme(to: newTheme)
applyThemeUpdatesToWindows()
}

public func systemThemeChanged() {
allWindowUUIDs.forEach { uuid in
// Ignore if:
// the system theme is off
// OR night mode is on
// OR private mode is on
guard systemThemeIsOn,
!nightModeIsOn,
!privateModeIsOn(for: uuid)
else { return }

changeCurrentTheme(getSystemThemeType(), for: uuid)
}
public func getUserManualTheme() -> ThemeType {
guard let savedThemeDescription = userDefaults.string(forKey: ThemeKeys.themeName),
let savedTheme = ThemeType(rawValue: savedThemeDescription)
else { return getThemeTypeBasedOnSystem() }

return savedTheme
}

// MARK: - System theme functions
public func setSystemTheme(isOn: Bool) {
userDefaults.set(isOn, forKey: ThemeKeys.systemThemeIsOn)
applyThemeUpdatesToWindows()
}

if isOn {
systemThemeChanged()
} else if automaticBrightnessIsOn {
updateThemeBasedOnBrightness()
} else {
allWindowUUIDs.forEach { reloadTheme(for: $0) }
}
private func getThemeTypeBasedOnSystem() -> ThemeType {
return UIScreen.main.traitCollection.userInterfaceStyle == .dark ? ThemeType.dark : ThemeType.light
}

// MARK: - Private theme functions
public func setPrivateTheme(isOn: Bool, for window: WindowUUID) {
let currentSetting = getPrivateThemeIsOn(for: window)
guard currentSetting != isOn else { return }
guard getPrivateThemeIsOn(for: window) != isOn else { return }

var settings: KeyedPrivateModeFlags
= userDefaults.object(forKey: ThemeKeys.PrivateMode.byWindowUUID) as? KeyedPrivateModeFlags ?? [:]

settings[window.uuidString] = NSNumber(value: isOn)
userDefaults.set(settings, forKey: ThemeKeys.PrivateMode.byWindowUUID)

updateCurrentTheme(to: fetchSavedThemeType(for: window), for: window)
applyThemeChanges(for: window, using: determineThemeType(for: window))
}

public func getPrivateThemeIsOn(for window: WindowUUID) -> Bool {
let settings = userDefaults.object(forKey: ThemeKeys.PrivateMode.byWindowUUID) as? KeyedPrivateModeFlags
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't need to block the PR but we probably don't need to store this in UserDefaults any longer, and changing it might also fix this issue where the windows are rendering briefly in Private theme incorrectly during launch:

launch_flash.mov

if settings == nil {
migrateSingleWindowPrivateDefaultsToMultiWindow(for: window)
}
if settings == nil { migrateSingleWindowPrivateDefaultsToMultiWindow(for: window) }

let boxedBool = settings?[window.uuidString] as? NSNumber
return boxedBool?.boolValue ?? false
}

// MARK: - Automatic brightness theme functions
public func setAutomaticBrightness(isOn: Bool) {
guard automaticBrightnessIsOn != isOn else { return }

userDefaults.set(isOn, forKey: ThemeKeys.AutomaticBrightness.isOn)
brightnessChanged()
applyThemeUpdatesToWindows()
}

public func setAutomaticBrightnessValue(_ value: Float) {
userDefaults.set(value, forKey: ThemeKeys.AutomaticBrightness.thresholdValue)
brightnessChanged()
applyThemeUpdatesToWindows()
}

private func getThemeTypeBasedOnBrightness() -> ThemeType {
return Float(UIScreen.main.brightness) < automaticBrightnessValue ? .dark : .light
}

public func brightnessChanged() {
if automaticBrightnessIsOn {
updateThemeBasedOnBrightness()
// MARK: - Window specific functions
public func windowNonspecificTheme() -> Theme {
switch getUserManualTheme() {
case .dark, .nightMode, .privateMode: return DarkTheme()
case .light: return LightTheme()
}
}

public func getNormalSavedTheme() -> ThemeType {
guard let savedThemeDescription = userDefaults.string(forKey: ThemeKeys.themeName),
let savedTheme = ThemeType(rawValue: savedThemeDescription)
else { return getSystemThemeType() }
public func windowDidClose(uuid: WindowUUID) {
windows.removeValue(forKey: uuid)
}

return savedTheme
public func setWindow(_ window: UIWindow, for uuid: WindowUUID) {
windows[uuid] = window
updateSavedTheme(to: getUserManualTheme())
applyThemeChanges(for: uuid, using: determineThemeType(for: uuid))
}

// MARK: - Private methods
// MARK: - Private helper methods

private func migrateSingleWindowPrivateDefaultsToMultiWindow(for window: WindowUUID) {
// Migrate old private setting to our window-based settings
Comment on lines 178 to 179
Copy link
Collaborator

@mattreaganmozilla mattreaganmozilla Jun 14, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we may be able to remove this migration code and persisted state entirely? IIRC this persisted state was to keep track of private mode between app launches, but now I think we've updated the iOS client to always clear private tabs on launch. And I believe the related setting for that has also been removed. (Though we should double-check, maybe all of this is still behind an experiment. I also have seen several user reviews already complaining about that change so we'd want to confirm with Product whether it's permanent or not.)

But if we have decided 100% to remove all of that, then I think we can also remove this migration and persisted state, since the app will never launch with any windows in Private mode.

I'd definitely like to get confirmation on that from someone who is a bit more in-the-loop on that however. (cc @OrlaM)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. I can make a note to fully look into this afterward, but I feel like this shouldn't be a blocker for merging this PR. It's easily removed later.

Expand All @@ -218,42 +183,36 @@ public final class DefaultThemeManager: ThemeManager, Notifiable {
}

private func updateSavedTheme(to newTheme: ThemeType) {
guard !newTheme.isOverridingThemeType() else { return }
guard !systemThemeIsOn else { return }
userDefaults.set(newTheme.rawValue, forKey: ThemeKeys.themeName)
}

private func updateCurrentTheme(to newTheme: ThemeType, for window: WindowUUID, notify: Bool = true) {
windowThemeState[window] = newThemeForType(newTheme)

private func applyThemeChanges(for window: WindowUUID, using newTheme: ThemeType) {
// Overwrite the user interface style on the window attached to our scene
// once we have multiple scenes we need to update all of them
let style = self.currentTheme(for: window).type.getInterfaceStyle()
let style = self.getCurrentTheme(for: window).type.getInterfaceStyle()
self.windows[window]?.overrideUserInterfaceStyle = style
if notify {
notifyCurrentThemeDidChange(for: window)
}
notifyCurrentThemeDidChange(for: window)
}

private func notifyCurrentThemeDidChange(for window: WindowUUID) {
mainQueue.ensureMainThread { [weak self] in
self?.notificationCenter.post(name: .ThemeDidChange, withUserInfo: window.userInfo)
self?.notificationCenter.post(
name: .ThemeDidChange,
withUserInfo: window.userInfo
)
}
}

private func fetchSavedThemeType(for window: WindowUUID) -> ThemeType {
if privateModeIsOn(for: window) { return .privateMode }
private func determineThemeType(for window: WindowUUID) -> ThemeType {
if getPrivateThemeIsOn(for: window) { return .privateMode }
if nightModeIsOn { return .nightMode }
if systemThemeIsOn { return getSystemThemeType() }
if systemThemeIsOn { return getThemeTypeBasedOnSystem() }
if automaticBrightnessIsOn { return getThemeTypeBasedOnBrightness() }

return getNormalSavedTheme()
return getUserManualTheme()
}

private func getSystemThemeType() -> ThemeType {
return UIScreen.main.traitCollection.userInterfaceStyle == .dark ? ThemeType.dark : ThemeType.light
}

private func newThemeForType(_ type: ThemeType) -> Theme {
private func getThemeFrom(type: ThemeType) -> Theme {
switch type {
case .light:
return LightTheme()
Expand All @@ -266,26 +225,13 @@ public final class DefaultThemeManager: ThemeManager, Notifiable {
}
}

private func updateThemeBasedOnBrightness() {
allWindowUUIDs.forEach { uuid in
let currentValue = Float(UIScreen.main.brightness)

if currentValue < automaticBrightnessValue {
changeCurrentTheme(.dark, for: uuid)
} else {
changeCurrentTheme(.light, for: uuid)
}
}
}

// MARK: - Notifiable

public func handleNotifications(_ notification: Notification) {
switch notification.name {
case UIScreen.brightnessDidChangeNotification:
brightnessChanged()
case UIApplication.didBecomeActiveNotification:
self.systemThemeChanged()
case UIScreen.brightnessDidChangeNotification,
UIApplication.didBecomeActiveNotification:
applyThemeUpdatesToWindows()
default:
return
}
Expand Down
10 changes: 4 additions & 6 deletions BrowserKit/Sources/Common/Theming/ThemeManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,22 @@ import UIKit

public protocol ThemeManager {
// Current theme
func currentTheme(for window: WindowUUID?) -> Theme
func getCurrentTheme(for window: WindowUUID?) -> Theme

// System theme and brightness settings
var systemThemeIsOn: Bool { get }
var automaticBrightnessIsOn: Bool { get }
var automaticBrightnessValue: Float { get }
func systemThemeChanged()
func setSystemTheme(isOn: Bool)
func setManualTheme(to newTheme: ThemeType)
func getUserManualTheme() -> ThemeType
func setAutomaticBrightness(isOn: Bool)
func setAutomaticBrightnessValue(_ value: Float)
func brightnessChanged()
func getNormalSavedTheme() -> ThemeType

// Window management and window-specific themeing
func changeCurrentTheme(_ newTheme: ThemeType, for window: WindowUUID)
func applyThemeUpdatesToWindows()
func setPrivateTheme(isOn: Bool, for window: WindowUUID)
func getPrivateThemeIsOn(for window: WindowUUID) -> Bool
func reloadTheme(for window: WindowUUID)
func setWindow(_ window: UIWindow, for uuid: WindowUUID)
func windowDidClose(uuid: WindowUUID)

Expand Down
10 changes: 0 additions & 10 deletions BrowserKit/Sources/Common/Theming/ThemeType.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,4 @@ public enum ThemeType: String {
case .light: UIBlurEffect.Style.extraLight
}
}

/// An overriding theme is a type of theme that overrides whatever theme
/// the user currently has selected, because they would be in a special
/// theming case, like private mode.
public func isOverridingThemeType() -> Bool {
switch self {
case .nightMode, .privateMode: return true
case .dark, .light: return false
}
}
}
2 changes: 1 addition & 1 deletion BrowserKit/Sources/Common/Theming/Themeable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ extension Themeable {
public func updateThemeApplicableSubviews(_ view: UIView, for window: WindowUUID?) {
guard let uuid = (view as? ThemeUUIDIdentifiable)?.currentWindowUUID ?? window else { return }
assert(uuid != .unavailable, "Theme applicable view has `unavailable` window UUID. Unexpected.")
let theme = themeManager.currentTheme(for: uuid)
let theme = themeManager.getCurrentTheme(for: uuid)
let themeViews = getAllSubviews(for: view, ofType: ThemeApplicable.self)
themeViews.forEach { $0.applyTheme(theme: theme) }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ public class BottomSheetViewController: UIViewController,

public func applyTheme() {
guard let uuid = (self.view as? ThemeUUIDIdentifiable)?.currentWindowUUID else { return }
contentView.backgroundColor = themeManager.currentTheme(for: uuid).colors.layer1
contentView.backgroundColor = themeManager.getCurrentTheme(for: uuid).colors.layer1
sheetView.layer.shadowOpacity = viewModel.shadowOpacity

if useDimmedBackground {
Expand Down
2 changes: 1 addition & 1 deletion firefox-ios/Client/AccessoryViewProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ class AccessoryViewProvider: UIView, Themeable, InjectedThemeUUIDIdentifiable {
}

func applyTheme() {
let theme = themeManager.currentTheme(for: windowUUID)
let theme = themeManager.getCurrentTheme(for: windowUUID)

backgroundColor = theme.colors.layer5
[previousButton, nextButton, doneButton].forEach {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ class CredentialAutofillCoordinator: BaseCoordinator {
// MARK: - Methods

private func currentTheme() -> Theme {
return themeManager.currentTheme(for: windowUUID)
return themeManager.getCurrentTheme(for: windowUUID)
}

func showCreditCardAutofill(creditCard: CreditCard?,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ struct SceneSetupHelper {
// Setting the initial theme correctly as we don't have a window attached yet to let ThemeManager set it
let themeManager: ThemeManager = AppContainer.shared.resolve()
themeManager.setWindow(window, for: windowUUID)
window.overrideUserInterfaceStyle = themeManager.currentTheme(for: windowUUID).type.getInterfaceStyle()
window.overrideUserInterfaceStyle = themeManager.getCurrentTheme(for: windowUUID).type.getInterfaceStyle()

return window
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ class SettingsCoordinator: BaseCoordinator,
let viewModel = WallpaperSettingsViewModel(
wallpaperManager: wallpaperManager,
tabManager: tabManager,
theme: themeManager.currentTheme(for: windowUUID)
theme: themeManager.getCurrentTheme(for: windowUUID)
)
let wallpaperVC = WallpaperSettingsViewController(viewModel: viewModel, windowUUID: windowUUID)
wallpaperVC.settingsDelegate = self
Expand Down
Loading
Loading