skip to Main Content

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?

2

Answers


  1. Chosen as BEST ANSWER

    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.


  2. 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.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search