Are changes to published embedded objects really not detected in SwiftUI?

Let's say I have a class to represent a user:

class User : Equatable, Codable, ObservableObject
{
	@Published var	ID: String = ""
	@Published var	username:  String = ""
	@Published var	firstName: String = ""
	@Published var	lastName: String = ""
	@Published var	EMail: String = ""
	@Published var	phoneNbr: String = ""
	@Published var	avatarURL: String = ""
	@Published var	mediaServiceID: String = ""
	@Published var	validated: Bool = false
...
}

I also have a controller ("viewmodel") to broker interactions between SwiftUI views and the User. It contains a User object as:

@MainActor class UserManager : ObservableObject
{
	@Published var user: User
...
}

And finally of course the view, into which I pass the UserManager upon initialization:

struct StartupView: View
{
	@ObservedObject var userMgr: UserManager
...
}

Changing published members of the User embedded in UserManager does not trigger a UI refresh. That strikes me as broken, since everything is published. Is it expected behavior?

Replies

This works as intended. Try if using the new Observation helps in your case.

  • Thanks for the reply, but how is that "as intended?" When something published changes, the UI is supposed to react.

  • Sorry, perhaps I misunderstood your code, but the old Observable API is known for not updating if model classes are nested. The new Observation API is better to detect changes in classes nested more than one level deep.

  • Thanks. Unfortunately I can't use that because I'm targeting back to iOS 15. I did have a look at it, though, and it too seems pretty incomplete. I appreciate your suggestion.

The view that owns the ObservableObject must use the @StateObject property.

struct StartupView: View
{
	@StateObject var userMgr: UserManager
...
}

Use @ObservedObject in any other views that need access to the user manager.

  • Thanks, but the UserManager is owned by the application, not a view.

  • Are you using @StateObject for the user manager in the App struct, which owns the user manager? If not, you should be.

Add a Comment

How about trying this?

@MainActor class UserManager: ObservableObject {
    @Published var user: User
    
    private var cancelSet: Set<AnyCancellable> = .init()
    
    init() {
        self.user = User()
        self.user.objectWillChange.sink { [weak self] in
            self?.objectWillChange.send()
        }
        .store(in: &cancelSet)
        Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
            Task { @MainActor in
                self.user.ID += "1"
            }
        }
    }
}



struct ContentView: View {
    @ObservedObject var userManager: UserManager
    
    var body: some View {
        VStack {
            Text("\(userManager.user.ID)")
        }
        .padding()
    }
}

  • Thanks for that. Just saw this. I've restructured some things, but I think using .sink to subscribe to an owned object's changes is still relevant.

Add a Comment