Skip to content

Commit a12d03f

Browse files
add helpers to observe theme changes
1 parent 303cbf0 commit a12d03f

File tree

2 files changed

+128
-0
lines changed

2 files changed

+128
-0
lines changed
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import SwiftUI
2+
3+
/// A SwiftUI wrapper that listens for theme changes and automatically refreshes its content.
4+
///
5+
/// `ThemeChangeObserver` ensures that its child views **rebuild** whenever the theme changes,
6+
/// helping to apply updated theme styles dynamically.
7+
///
8+
/// ## Usage
9+
///
10+
/// Wrap your view inside `ThemeChangeObserver` to make it responsive to theme updates:
11+
///
12+
/// ```swift
13+
/// @main
14+
/// struct Root: App {
15+
/// var body: some Scene {
16+
/// WindowGroup {
17+
/// ThemeChangeObserver {
18+
/// Content()
19+
/// }
20+
/// }
21+
/// }
22+
/// }
23+
/// ```
24+
///
25+
/// ## Performance Considerations
26+
///
27+
/// - This approach forces a **full re-evaluation** of the wrapped content, which ensures all theme-dependent
28+
/// properties are updated.
29+
/// - Use it **at a high level** in your SwiftUI hierarchy (e.g., wrapping entire screens) rather than for small components.
30+
public struct ThemeChangeObserver<Content: View>: View {
31+
@State private var themeId = UUID()
32+
@ViewBuilder var content: () -> Content
33+
34+
public init(content: @escaping () -> Content) {
35+
self.content = content
36+
}
37+
38+
public var body: some View {
39+
self.content()
40+
.onReceive(NotificationCenter.default.publisher(
41+
for: Theme.didChangeThemeNotification,
42+
object: nil
43+
)) { _ in
44+
self.themeId = UUID()
45+
}
46+
.id(self.themeId)
47+
}
48+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import Foundation
2+
import Combine
3+
4+
extension NSObject {
5+
/// Observes changes to the `.current` theme and updates dependent views.
6+
///
7+
/// This method allows you to respond to theme changes by updating view properties that depend on the theme.
8+
///
9+
/// You can invoke the ``observeThemeChange(_:)`` method a single time in the `viewDidLoad`
10+
/// and update all the view elements:
11+
///
12+
/// ```swift
13+
/// override func viewDidLoad() {
14+
/// super.viewDidLoad()
15+
///
16+
/// style()
17+
///
18+
/// observeThemeChanges { [weak self] in
19+
/// guard let self else { return }
20+
///
21+
/// self.style()
22+
/// }
23+
/// }
24+
///
25+
/// func style() {
26+
/// view.backgroundColor = UniversalColor.background.uiColor
27+
/// // ...
28+
/// }
29+
/// ```
30+
///
31+
/// > Note: There is no need to update components from the library as they observe the changes internally.
32+
///
33+
/// ## Cancellation
34+
///
35+
/// The method returns an ``AnyCancellable`` that can be used to cancel observation. For
36+
/// example, if you only want to observe while a view controller is visible, you can start
37+
/// observation in the `viewWillAppear` and then cancel observation in the `viewWillDisappear`:
38+
///
39+
/// ```swift
40+
/// var cancellable: AnyCancellable?
41+
///
42+
/// func viewWillAppear() {
43+
/// super.viewWillAppear()
44+
/// cancellable = observeThemeChange { [weak self] in
45+
/// // ...
46+
/// }
47+
/// }
48+
/// func viewWillDisappear() {
49+
/// super.viewWillDisappear()
50+
/// cancellable?.cancel()
51+
/// }
52+
/// ```
53+
///
54+
/// - Parameter apply: A closure that will be called whenever the `.current` theme changes.
55+
/// This should contain logic to update theme-dependent views.
56+
/// - Returns: An `AnyCancellable` instance that can be used to stop observing the theme changes when needed.
57+
@discardableResult
58+
public func observeThemeChange(_ apply: @escaping () -> Void) -> AnyCancellable {
59+
let cancellable = NotificationCenter.default.publisher(
60+
for: Theme.didChangeThemeNotification
61+
)
62+
.receive(on: DispatchQueue.main)
63+
.sink { _ in
64+
apply()
65+
}
66+
self.cancellables.append(cancellable)
67+
return cancellable
68+
}
69+
70+
fileprivate var cancellables: [Any] {
71+
get {
72+
objc_getAssociatedObject(self, Self.cancellablesKey) as? [Any] ?? []
73+
}
74+
set {
75+
objc_setAssociatedObject(self, Self.cancellablesKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
76+
}
77+
}
78+
79+
private static let cancellablesKey = "themeChangeObserverCancellables"
80+
}

0 commit comments

Comments
 (0)