Home > Blockchain >  How can I invalidate a SwiftUI View in response to a notification?
How can I invalidate a SwiftUI View in response to a notification?

Time:12-07

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.

  • Related