Add honeycomb pattern toggle and make theme switching reactive
Adds a toggleable honeycomb hexagonal grid overlay (matching the website pattern) that can be enabled independently of any theme via the Appearance screen. Uses a cached tiled UIImage approach consistent with the existing grain texture system. Replaces the destructive refreshID-based theme switching (which destroyed all NavigationStacks and dismissed sheets) with @Observable AppThemeSource. Color resolution now happens through Swift's Observation framework, so all views using Color.appPrimary etc. automatically re-render when the theme changes — no view identity reset needed. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -47,4 +47,11 @@ struct ThemeIDTests {
|
|||||||
@Test func oceanRawValueIsOcean() {
|
@Test func oceanRawValueIsOcean() {
|
||||||
#expect(ThemeID.ocean.rawValue == "Ocean")
|
#expect(ThemeID.ocean.rawValue == "Ocean")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Honeycomb Setting Tests
|
||||||
|
|
||||||
|
@Test func honeycombDefaultsToDisabled() {
|
||||||
|
// HoneycombSetting reads from UserDefaults which defaults to false for unset Bool keys
|
||||||
|
#expect(HoneycombSetting.isEnabled == false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ enum AnalyticsEvent {
|
|||||||
|
|
||||||
// MARK: - Settings
|
// MARK: - Settings
|
||||||
case themeChanged(theme: String)
|
case themeChanged(theme: String)
|
||||||
|
case honeycombToggled(enabled: Bool)
|
||||||
case analyticsToggled(enabled: Bool)
|
case analyticsToggled(enabled: Bool)
|
||||||
|
|
||||||
// MARK: - Errors
|
// MARK: - Errors
|
||||||
@@ -82,6 +83,8 @@ enum AnalyticsEvent {
|
|||||||
// Settings
|
// Settings
|
||||||
case .themeChanged(let theme):
|
case .themeChanged(let theme):
|
||||||
return ("theme_changed", ["theme": theme])
|
return ("theme_changed", ["theme": theme])
|
||||||
|
case .honeycombToggled(let enabled):
|
||||||
|
return ("honeycomb_toggled", ["enabled": enabled])
|
||||||
case .analyticsToggled(let enabled):
|
case .analyticsToggled(let enabled):
|
||||||
return ("analytics_toggled", ["enabled": enabled])
|
return ("analytics_toggled", ["enabled": enabled])
|
||||||
|
|
||||||
|
|||||||
@@ -3,29 +3,60 @@ import SwiftUI
|
|||||||
// MARK: - Design System
|
// MARK: - Design System
|
||||||
// Modern, sleek design system for honeyDue with Light and Dark mode support
|
// Modern, sleek design system for honeyDue with Light and Dark mode support
|
||||||
|
|
||||||
|
// MARK: - Reactive Theme Source
|
||||||
|
|
||||||
|
#if WIDGET_EXTENSION
|
||||||
|
// Widgets: read directly from UserDefaults (no reactivity needed)
|
||||||
|
enum AppThemeSource {
|
||||||
|
private static let defaults: UserDefaults = {
|
||||||
|
let groupID = Bundle.main.infoDictionary?["AppGroupIdentifier"] as? String
|
||||||
|
return groupID.flatMap { UserDefaults(suiteName: $0) } ?? .standard
|
||||||
|
}()
|
||||||
|
|
||||||
|
static var themeName: String {
|
||||||
|
defaults.string(forKey: "selectedTheme") ?? ThemeID.bright.rawValue
|
||||||
|
}
|
||||||
|
|
||||||
|
static var honeycombEnabled: Bool {
|
||||||
|
defaults.bool(forKey: "honeycombEnabled")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
/// Observable theme source that SwiftUI automatically tracks.
|
||||||
|
/// When `themeName` changes, all views using `Color.appPrimary` etc. re-render.
|
||||||
|
@Observable
|
||||||
|
final class AppThemeSource {
|
||||||
|
static let shared = AppThemeSource()
|
||||||
|
|
||||||
|
var themeName: String
|
||||||
|
var honeycombEnabled: Bool
|
||||||
|
|
||||||
|
private init() {
|
||||||
|
let groupID = Bundle.main.infoDictionary?["AppGroupIdentifier"] as? String
|
||||||
|
let defaults = groupID.flatMap { UserDefaults(suiteName: $0) } ?? .standard
|
||||||
|
self.themeName = defaults.string(forKey: "selectedTheme") ?? ThemeID.bright.rawValue
|
||||||
|
self.honeycombEnabled = defaults.bool(forKey: "honeycombEnabled")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
// MARK: - Colors
|
// MARK: - Colors
|
||||||
|
|
||||||
extension Color {
|
extension Color {
|
||||||
// MARK: - Dynamic Theme Resolution
|
// MARK: - Dynamic Theme Resolution
|
||||||
|
|
||||||
/// Shared App Group defaults for reading the active theme.
|
|
||||||
/// Thread-safe: UserDefaults is safe to read from any thread/actor.
|
|
||||||
private static let _themeDefaults: UserDefaults = {
|
|
||||||
let groupID = Bundle.main.infoDictionary?["AppGroupIdentifier"] as? String
|
|
||||||
return groupID.flatMap { UserDefaults(suiteName: $0) } ?? .standard
|
|
||||||
}()
|
|
||||||
|
|
||||||
private static func themed(_ name: String) -> Color {
|
private static func themed(_ name: String) -> Color {
|
||||||
// Read theme directly from shared UserDefaults instead of going through
|
#if WIDGET_EXTENSION
|
||||||
// @MainActor-isolated ThemeManager.shared. This is safe to call from any
|
let theme = AppThemeSource.themeName
|
||||||
// actor context (including widget timeline providers and background threads).
|
#else
|
||||||
let theme = _themeDefaults.string(forKey: "selectedTheme") ?? ThemeID.bright.rawValue
|
let theme = AppThemeSource.shared.themeName
|
||||||
|
#endif
|
||||||
return Color("\(theme)/\(name)", bundle: nil)
|
return Color("\(theme)/\(name)", bundle: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Semantic Colors (Use These in UI)
|
// MARK: - Semantic Colors (Use These in UI)
|
||||||
// These dynamically resolve based on the active theme stored in App Group UserDefaults.
|
// Main app: resolved via @Observable AppThemeSource — views auto-update on theme change.
|
||||||
// Safe to call from any actor context (main app, widget extensions, background threads).
|
// Widgets: resolved via UserDefaults (static, no reactivity needed).
|
||||||
static var appPrimary: Color { themed("Primary") }
|
static var appPrimary: Color { themed("Primary") }
|
||||||
static var appSecondary: Color { themed("Secondary") }
|
static var appSecondary: Color { themed("Secondary") }
|
||||||
static var appAccent: Color { themed("Accent") }
|
static var appAccent: Color { themed("Accent") }
|
||||||
|
|||||||
@@ -4,6 +4,18 @@ import UIKit
|
|||||||
// MARK: - Organic Design System
|
// MARK: - Organic Design System
|
||||||
// Warm, natural aesthetic with soft shapes, subtle textures, and flowing layouts
|
// Warm, natural aesthetic with soft shapes, subtle textures, and flowing layouts
|
||||||
|
|
||||||
|
// MARK: - Honeycomb Setting Reader
|
||||||
|
/// Reads honeycomb enabled state from AppThemeSource (reactive in main app, static in widgets).
|
||||||
|
enum HoneycombSetting {
|
||||||
|
static var isEnabled: Bool {
|
||||||
|
#if WIDGET_EXTENSION
|
||||||
|
AppThemeSource.honeycombEnabled
|
||||||
|
#else
|
||||||
|
AppThemeSource.shared.honeycombEnabled
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Organic Shapes
|
// MARK: - Organic Shapes
|
||||||
|
|
||||||
/// Soft organic blob shape for backgrounds
|
/// Soft organic blob shape for backgrounds
|
||||||
@@ -140,6 +152,80 @@ private final class GrainTextureCache {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Honeycomb Texture Cache
|
||||||
|
|
||||||
|
/// Generates and caches a honeycomb hexagonal grid tile image.
|
||||||
|
/// Matches the website pattern: 60×103.92px tile, #C4856A stroke, 0.8px width.
|
||||||
|
private final class HoneycombTextureCache {
|
||||||
|
static let shared = HoneycombTextureCache()
|
||||||
|
|
||||||
|
private(set) var cachedImage: UIImage?
|
||||||
|
private let tileSize = CGSize(width: 60, height: 103.92)
|
||||||
|
private let lock = NSLock()
|
||||||
|
|
||||||
|
private init() {
|
||||||
|
generateIfNeeded()
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateIfNeeded() {
|
||||||
|
lock.lock()
|
||||||
|
defer { lock.unlock() }
|
||||||
|
guard cachedImage == nil else { return }
|
||||||
|
|
||||||
|
let w = tileSize.width
|
||||||
|
let h = tileSize.height
|
||||||
|
let strokeColor = UIColor(red: 196/255, green: 133/255, blue: 106/255, alpha: 1.0) // #C4856A
|
||||||
|
let strokeWidth: CGFloat = 0.8
|
||||||
|
|
||||||
|
let renderer = UIGraphicsImageRenderer(size: tileSize)
|
||||||
|
cachedImage = renderer.image { ctx in
|
||||||
|
let cgCtx = ctx.cgContext
|
||||||
|
cgCtx.setStrokeColor(strokeColor.cgColor)
|
||||||
|
cgCtx.setLineWidth(strokeWidth)
|
||||||
|
cgCtx.setFillColor(UIColor.clear.cgColor)
|
||||||
|
|
||||||
|
// First hexagon (top)
|
||||||
|
let hex1 = CGMutablePath()
|
||||||
|
hex1.move(to: CGPoint(x: 30, y: 0))
|
||||||
|
hex1.addLine(to: CGPoint(x: 60, y: 17.32))
|
||||||
|
hex1.addLine(to: CGPoint(x: 60, y: 51.96))
|
||||||
|
hex1.addLine(to: CGPoint(x: 30, y: 69.28))
|
||||||
|
hex1.addLine(to: CGPoint(x: 0, y: 51.96))
|
||||||
|
hex1.addLine(to: CGPoint(x: 0, y: 17.32))
|
||||||
|
hex1.closeSubpath()
|
||||||
|
cgCtx.addPath(hex1)
|
||||||
|
cgCtx.strokePath()
|
||||||
|
|
||||||
|
// Second hexagon (bottom-right, offset for tessellation)
|
||||||
|
let hex2 = CGMutablePath()
|
||||||
|
hex2.move(to: CGPoint(x: 60, y: 51.96))
|
||||||
|
hex2.addLine(to: CGPoint(x: 90, y: 69.28))
|
||||||
|
hex2.addLine(to: CGPoint(x: 90, y: h))
|
||||||
|
hex2.addLine(to: CGPoint(x: 60, y: h + 17.32))
|
||||||
|
hex2.addLine(to: CGPoint(x: 30, y: h))
|
||||||
|
hex2.addLine(to: CGPoint(x: 30, y: 69.28))
|
||||||
|
hex2.closeSubpath()
|
||||||
|
cgCtx.addPath(hex2)
|
||||||
|
cgCtx.strokePath()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Honeycomb Texture Overlay
|
||||||
|
|
||||||
|
struct HoneycombTexture: View {
|
||||||
|
var opacity: Double = 0.10
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
if let uiImage = HoneycombTextureCache.shared.cachedImage {
|
||||||
|
Image(uiImage: uiImage)
|
||||||
|
.resizable(resizingMode: .tile)
|
||||||
|
.opacity(opacity)
|
||||||
|
.allowsHitTesting(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Grain Texture Overlay
|
// MARK: - Grain Texture Overlay
|
||||||
|
|
||||||
struct GrainTexture: View {
|
struct GrainTexture: View {
|
||||||
@@ -193,6 +279,12 @@ struct OrganicCardBackground: View {
|
|||||||
// Grain texture
|
// Grain texture
|
||||||
GrainTexture(opacity: 0.015)
|
GrainTexture(opacity: 0.015)
|
||||||
.clipShape(RoundedRectangle(cornerRadius: 28, style: .continuous))
|
.clipShape(RoundedRectangle(cornerRadius: 28, style: .continuous))
|
||||||
|
|
||||||
|
// Honeycomb pattern overlay
|
||||||
|
if HoneycombSetting.isEnabled {
|
||||||
|
HoneycombTexture(opacity: 0.10)
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: 28, style: .continuous))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -385,6 +477,11 @@ struct WarmGradientBackground: View {
|
|||||||
|
|
||||||
// Grain for natural feel
|
// Grain for natural feel
|
||||||
GrainTexture(opacity: 0.02)
|
GrainTexture(opacity: 0.02)
|
||||||
|
|
||||||
|
// Honeycomb pattern overlay
|
||||||
|
if HoneycombSetting.isEnabled {
|
||||||
|
HoneycombTexture(opacity: 0.10)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.ignoresSafeArea()
|
.ignoresSafeArea()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,4 +10,7 @@ extension Notification.Name {
|
|||||||
|
|
||||||
// Task action completion notification (for UI refresh)
|
// Task action completion notification (for UI refresh)
|
||||||
static let taskActionCompleted = Notification.Name("taskActionCompleted")
|
static let taskActionCompleted = Notification.Name("taskActionCompleted")
|
||||||
|
|
||||||
|
// Appearance changed — posted when the settings sheet dismisses after theme/honeycomb changes
|
||||||
|
static let appearanceDidFinishChanging = Notification.Name("appearanceDidFinishChanging")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -88,7 +88,12 @@ class ThemeManager {
|
|||||||
return .bright
|
return .bright
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var honeycombEnabled: Bool {
|
||||||
|
sharedDefaults.bool(forKey: honeycombKey)
|
||||||
|
}
|
||||||
|
|
||||||
private let themeKey = "selectedTheme"
|
private let themeKey = "selectedTheme"
|
||||||
|
private let honeycombKey = "honeycombEnabled"
|
||||||
|
|
||||||
private init() {}
|
private init() {}
|
||||||
}
|
}
|
||||||
@@ -104,7 +109,14 @@ class ThemeManager: ObservableObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Published var honeycombEnabled: Bool {
|
||||||
|
didSet {
|
||||||
|
sharedDefaults.set(honeycombEnabled, forKey: honeycombKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private let themeKey = "selectedTheme"
|
private let themeKey = "selectedTheme"
|
||||||
|
private let honeycombKey = "honeycombEnabled"
|
||||||
|
|
||||||
private init() {
|
private init() {
|
||||||
// Load saved theme from shared App Group defaults
|
// Load saved theme from shared App Group defaults
|
||||||
@@ -114,17 +126,24 @@ class ThemeManager: ObservableObject {
|
|||||||
} else {
|
} else {
|
||||||
self.currentTheme = .bright
|
self.currentTheme = .bright
|
||||||
}
|
}
|
||||||
|
self.honeycombEnabled = sharedDefaults.bool(forKey: honeycombKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func saveTheme() {
|
private func saveTheme() {
|
||||||
// Save to shared App Group defaults so widgets can access it
|
// Save to shared App Group defaults so widgets can access it
|
||||||
sharedDefaults.set(currentTheme.rawValue, forKey: themeKey)
|
sharedDefaults.set(currentTheme.rawValue, forKey: themeKey)
|
||||||
|
// Update reactive source so all views using Color.appPrimary etc. re-render
|
||||||
|
AppThemeSource.shared.themeName = currentTheme.rawValue
|
||||||
}
|
}
|
||||||
|
|
||||||
func setTheme(_ theme: ThemeID) {
|
func setTheme(_ theme: ThemeID) {
|
||||||
withAnimation(.easeInOut(duration: 0.3)) {
|
currentTheme = theme
|
||||||
currentTheme = theme
|
}
|
||||||
}
|
|
||||||
|
func setHoneycomb(_ enabled: Bool) {
|
||||||
|
honeycombEnabled = enabled
|
||||||
|
// Update reactive source so honeycomb overlays re-render
|
||||||
|
AppThemeSource.shared.honeycombEnabled = enabled
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -65,34 +65,11 @@ struct MainTabView: View {
|
|||||||
.onChange(of: authManager.isAuthenticated) { _, _ in
|
.onChange(of: authManager.isAuthenticated) { _, _ in
|
||||||
selectedTab = .residences
|
selectedTab = .residences
|
||||||
}
|
}
|
||||||
|
.onChange(of: themeManager.currentTheme) { _, _ in
|
||||||
|
updateTabBarAppearance()
|
||||||
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
// FIX_SKIPPED(F-10): UITabBar.appearance() is the standard SwiftUI pattern
|
updateTabBarAppearance()
|
||||||
// for customizing tab bar appearance. The global side effect persists but
|
|
||||||
// there is no safe alternative without UIKit hosting.
|
|
||||||
|
|
||||||
// Configure tab bar appearance
|
|
||||||
let appearance = UITabBarAppearance()
|
|
||||||
appearance.configureWithOpaqueBackground()
|
|
||||||
|
|
||||||
// Use theme-aware colors
|
|
||||||
appearance.backgroundColor = UIColor(Color.appBackgroundSecondary)
|
|
||||||
|
|
||||||
// Selected item — uses Dynamic Type caption2 style (A-2)
|
|
||||||
appearance.stackedLayoutAppearance.selected.iconColor = UIColor(Color.appPrimary)
|
|
||||||
appearance.stackedLayoutAppearance.selected.titleTextAttributes = [
|
|
||||||
.foregroundColor: UIColor(Color.appPrimary),
|
|
||||||
.font: UIFont.preferredFont(forTextStyle: .caption2)
|
|
||||||
]
|
|
||||||
|
|
||||||
// Normal item — uses Dynamic Type caption2 style (A-2)
|
|
||||||
appearance.stackedLayoutAppearance.normal.iconColor = UIColor(Color.appTextSecondary)
|
|
||||||
appearance.stackedLayoutAppearance.normal.titleTextAttributes = [
|
|
||||||
.foregroundColor: UIColor(Color.appTextSecondary),
|
|
||||||
.font: UIFont.preferredFont(forTextStyle: .caption2)
|
|
||||||
]
|
|
||||||
|
|
||||||
UITabBar.appearance().standardAppearance = appearance
|
|
||||||
UITabBar.appearance().scrollEdgeAppearance = appearance
|
|
||||||
|
|
||||||
// Handle pending navigation from push notification
|
// Handle pending navigation from push notification
|
||||||
if pushManager.pendingNavigationTaskId != nil {
|
if pushManager.pendingNavigationTaskId != nil {
|
||||||
@@ -119,6 +96,27 @@ struct MainTabView: View {
|
|||||||
selectedTab = .residences
|
selectedTab = .residences
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func updateTabBarAppearance() {
|
||||||
|
let appearance = UITabBarAppearance()
|
||||||
|
appearance.configureWithOpaqueBackground()
|
||||||
|
appearance.backgroundColor = UIColor(Color.appBackgroundSecondary)
|
||||||
|
|
||||||
|
appearance.stackedLayoutAppearance.selected.iconColor = UIColor(Color.appPrimary)
|
||||||
|
appearance.stackedLayoutAppearance.selected.titleTextAttributes = [
|
||||||
|
.foregroundColor: UIColor(Color.appPrimary),
|
||||||
|
.font: UIFont.preferredFont(forTextStyle: .caption2)
|
||||||
|
]
|
||||||
|
|
||||||
|
appearance.stackedLayoutAppearance.normal.iconColor = UIColor(Color.appTextSecondary)
|
||||||
|
appearance.stackedLayoutAppearance.normal.titleTextAttributes = [
|
||||||
|
.foregroundColor: UIColor(Color.appTextSecondary),
|
||||||
|
.font: UIFont.preferredFont(forTextStyle: .caption2)
|
||||||
|
]
|
||||||
|
|
||||||
|
UITabBar.appearance().standardAppearance = appearance
|
||||||
|
UITabBar.appearance().scrollEdgeAppearance = appearance
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#Preview {
|
#Preview {
|
||||||
|
|||||||
@@ -11,6 +11,46 @@ struct ThemeSelectionView: View {
|
|||||||
.ignoresSafeArea()
|
.ignoresSafeArea()
|
||||||
|
|
||||||
List {
|
List {
|
||||||
|
// Honeycomb pattern toggle
|
||||||
|
Section {
|
||||||
|
HStack(spacing: 14) {
|
||||||
|
ZStack {
|
||||||
|
RoundedRectangle(cornerRadius: 10, style: .continuous)
|
||||||
|
.fill(Color.appPrimary.opacity(0.12))
|
||||||
|
.frame(width: 40, height: 40)
|
||||||
|
Image(systemName: "hexagon")
|
||||||
|
.font(.system(size: 18, weight: .semibold))
|
||||||
|
.foregroundColor(Color.appPrimary)
|
||||||
|
}
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 3) {
|
||||||
|
Text("Honeycomb Pattern")
|
||||||
|
.font(.system(size: 16, weight: .semibold, design: .rounded))
|
||||||
|
.foregroundColor(Color.appTextPrimary)
|
||||||
|
Text("Adds a subtle hexagonal grid overlay")
|
||||||
|
.font(.system(size: 12, weight: .medium))
|
||||||
|
.foregroundColor(Color.appTextSecondary)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Toggle("", isOn: Binding(
|
||||||
|
get: { themeManager.honeycombEnabled },
|
||||||
|
set: { newValue in
|
||||||
|
let generator = UIImpactFeedbackGenerator(style: .light)
|
||||||
|
generator.impactOccurred()
|
||||||
|
AnalyticsManager.shared.track(.honeycombToggled(enabled: newValue))
|
||||||
|
themeManager.setHoneycomb(newValue)
|
||||||
|
}
|
||||||
|
))
|
||||||
|
.labelsHidden()
|
||||||
|
.tint(Color.appPrimary)
|
||||||
|
}
|
||||||
|
.padding(.vertical, 4)
|
||||||
|
}
|
||||||
|
.sectionBackground()
|
||||||
|
|
||||||
|
// Theme list
|
||||||
ForEach(ThemeID.allCases, id: \.self) { theme in
|
ForEach(ThemeID.allCases, id: \.self) { theme in
|
||||||
Button(action: {
|
Button(action: {
|
||||||
selectTheme(theme)
|
selectTheme(theme)
|
||||||
@@ -46,8 +86,12 @@ struct ThemeSelectionView: View {
|
|||||||
// Track theme change
|
// Track theme change
|
||||||
AnalyticsManager.shared.track(.themeChanged(theme: theme.rawValue))
|
AnalyticsManager.shared.track(.themeChanged(theme: theme.rawValue))
|
||||||
|
|
||||||
// Update theme with animation
|
// Set theme without animation to prevent list rows from being removed/shuffled
|
||||||
themeManager.setTheme(theme)
|
var transaction = Transaction()
|
||||||
|
transaction.disablesAnimations = true
|
||||||
|
withTransaction(transaction) {
|
||||||
|
themeManager.currentTheme = theme
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -190,9 +190,6 @@ struct RootView: View {
|
|||||||
// Show main app
|
// Show main app
|
||||||
ZStack(alignment: .topLeading) {
|
ZStack(alignment: .topLeading) {
|
||||||
MainTabView(refreshID: refreshID)
|
MainTabView(refreshID: refreshID)
|
||||||
.onChange(of: themeManager.currentTheme) { _, _ in
|
|
||||||
refreshID = UUID()
|
|
||||||
}
|
|
||||||
Color.clear
|
Color.clear
|
||||||
.frame(width: 1, height: 1)
|
.frame(width: 1, height: 1)
|
||||||
.accessibilityIdentifier("ui.root.mainTabs")
|
.accessibilityIdentifier("ui.root.mainTabs")
|
||||||
|
|||||||
Reference in New Issue
Block a user