Why does SwiftUI not update a view here?

Viewed 58

The background color and text in ReadyDashboardView below don't update when isConnected is updated. Obviously I want to update the view when the connection is made. I was expecting that publishing everything in the chain to that variable would make it update instantly in swiftui. Instead, it's always rendering using the value provided when the variable is instantiated. Here's a very simplified look at the situation:

Is there another swiftui feature i should be using or am I going to have to make some sweeping changes to my codebase?

import SwiftUI

@main
struct TestApp: App {
    @StateObject private var env = PrinterEnv()
    
    var body: some Scene {
        WindowGroup {
            ReadyDashboardView()
                .environmentObject(env)
        }
    }
}

struct ReadyDashboardView: View {
    @EnvironmentObject var env: PrinterEnv

    var body: some View {
        VStack {
            HStack {
                Spacer()
                VStack {
                    Text(env.selectedPrinter?.isConnected ?? false ? "Printer Ready" : "Not Connected")
                        .padding(.bottom)
                }
                Spacer()
            }
            .background(env.selectedPrinter?.isConnected ?? false ? .green : .red)
           
        Button("Connect") { env.selectedPrinter?.isConnected = true }
        Button("Disconnect") { env.selectedPrinter?.isConnected = false }
            
        }
    }
}

class PrinterEnv: ObservableObject {
    @Published var configuredPrinters: [Printer] = []
    @Published var selectedPrinter: Printer?
    
    init() {
        configuredPrinters.append(contentsOf: [Printer()])
        selectedPrinter = configuredPrinters.first
    }
}

class Printer: ObservableObject {
    @Published var isConnected = false
}
2 Answers

I suggest you do not nest ObservableObject, it does not work very well.

Try a Printer struct for example, such as:

struct Printer: Identifiable {
    let id = UUID()
    var isConnected = false
}

I do not think the problem is specifically related to nested observable objects. Nesting them is working fine and is even recommended by community members as the best way to manage app wide states and ensure performance is acceptable.

See this: https://www.fivestars.blog/articles/app-state/

That said, I believe it's probably related to the wrapping of the observable object with @Published property wrapper.

Try this in a playground:

import SwiftUI

class AppState: ObservableObject {
    let fooState = FooState()
    let barState = BarState()
}

class FooState: ObservableObject {
    @Published var foo: Int = 42
}

class BarState: ObservableObject {
    @Published var bar: String = Date().debugDescription
}

struct FooView: View {

    @EnvironmentObject var fooState: FooState

    var body: some View {
        Text("foo: \(fooState.foo)")
    }
}

struct BarView: View {

    @EnvironmentObject var barState: BarState

    var body: some View {
        Text("bar: \(barState.bar)")
    }
}

struct ContentView: View {

    @EnvironmentObject var appState: AppState

    var body: some View {
        VStack {
            Spacer()

            FooView()
                .environmentObject(appState.fooState)

            Button("update foo") {
                appState.fooState.foo = Int.random(in: 1...100)
            }

            Spacer()

            BarView()
                .environmentObject(appState.barState)

            Button("update bar") {
                appState.barState.bar = Date().debugDescription
            }

            Spacer()
        }
    }
}

import PlaygroundSupport

PlaygroundPage.current.liveView = UIHostingController(
    rootView: ContentView()
        .frame(width: 320, height: 414)
        .environmentObject(AppState())
)

enter image description here

Related