I'm new to SwiftUI and trying to start using it in a complex, existing UIKit app. The app has a theming system, and I'm not sure how to get the SwiftUI view to respond to theme change events.
Our theme objects look like
class ThemeService {
static var textColor: UIColor { get }
}
struct ViewTheme: {
private(set) var textColor = { ThemeService.textColor }
}
where the value returned ThemeService.textColor
changes when the user changes the app's theme. In the UIKit portions of the app, views observe a "themeChanged"
Notification
and re-read the value of the textColor
property from their theme structs.
I'm not sure how to manage this for SwiftUI. Since ViewTheme
isn't an object, I can't use @ObservableObject
, but its textColor
property also doesn't change when the theme changes; just the value returned by calling it changes.
Is there a way to somehow get SwiftUI to re-render the view hierarchy from an external event, rather than from a change in a value that the view sees? Or should I be approaching this differently?
CodePudding user response:
I was able to get this working by cheating with the theming system a little and changing the ViewTheme
to an object:
class ViewTheme: ObservableObject {
private(set) var textColor = { ThemeService.textColor }
init() {
NotificationCenter.default.addObserver(forName: Notification.Name("themeChanged"),
object: nil, queue: .main) { [weak self] _ in
self?.objectWillChange.send()
}
}
}
Now the view can mark it with @ObservedObject
and will be re-generated when the "themeChanged"
notification is fired.
This seems to be working great, but I'm not sure if there are non-obvious problems that I'm missing with this solution.
CodePudding user response:
Your answer works perfectly well, but it requires adoption of ObservableObject. Here is an alternative answer which uses your existing NotificationCenter notifications to update a SwiftUI view.
struct MyView: View {
@State private var textColor: UIColor
var body: some View {
Text("Hello")
.foregroundColor(Color(textColor))
.onReceive(NotificationCenter.default.publisher(for: Notification.Name("themeChanged"))) { _ in
textColor = ThemeService.textColor
}
}
}
This requires a @State variable to hold the theme's current data, but it's correct because a SwiftUI view is really just a snapshot of what the view should currently display. It ideally should not reference data that is arbitrarily changed because it leads to data-sync problems like the question you asked. So in a SwiftUI view, it is problematic to write ThemeService.textColor
directly within the body unless you are certain an update will always occur after it the theme gets changed.