Can A Property In An ObservableObject listen to Another Property? How?

Hi. this is my observable object

final class ToolbarItemProperties: ObservableObject {
  var visible = false
  var disabled = false
  var icon = "questionmark"
  var action: () -> Void = { }
  var opacity: Double = 1
}

the opacity property value should either be 0.5 or 1 depending on the value of the property disabled. How to listen to it?

The purpose for this is because if i set a button to disabled, it does not render any way to the user to let them know it is disabled. So i thought of changing the opacity instead, hence the opacity property.

So if i do something like this

item.disabled = true

the opacity property value should become 0.5. if disabled is set to false, the opacity value will be 1.

Accepted Reply

The issue you have is that ToolbarItemProperties is a class, and an instance of that class is stored in an @Published variable inside of an ObservableObject class. This AppData class is expected to watch the toolbarItem1 property for any changes (and then update the UI) but, because of the nature of a class, the difference between one before and after a variable change isn't as easily noticeable. If ToolbarItemProperties was a struct this wouldn't be a problem.

Because of this, you will need to manually publish changes to tell SwiftUI in advance that a variable is going to change and to reload the UI. This is done by using the provided property of the ObservableObject protocol:

objectWillChange.send()



Since you didn't provide any example code, I have created some which shows this working:

final class ToolbarItemProperties {
    var visible = false
    var disabled = false
    var icon = "questionmark"
    var action = {}

    var opacity: Double {
        disabled ? 0.5 : 1
    }
}

final class AppData: ObservableObject {
    @Published var toolbarItem1: ToolbarItemProperties?
}

struct ContentView: View {
    @StateObject private var appData: AppData

    init() {
        let appData = AppData()
        appData.toolbarItem1 = ToolbarItemProperties()
        _appData = StateObject(wrappedValue: appData)
    }

    var body: some View {
        VStack(spacing: 20) {
            Text("Hello, World!")
                .opacity(appData.toolbarItem1?.opacity ?? 1)

            let toggleBinding = Binding<Bool> {
                appData.toolbarItem1?.disabled == true
            } set: {
                // This line is the key part
                // Must be before the variable change
                appData.objectWillChange.send()
                
                appData.toolbarItem1?.disabled = $0
            }

            Toggle("Is Disabled", isOn: toggleBinding)
                .toggleStyle(.button)
        }
    }
}
  • ill get back to you in a few days since im out of town now. i actually also tried the manual trigger to no avail but it looks like the way you did it is different from what i did.

    ill check this one out and compare. thank you.

Add a Comment

Replies

Use a computed property :

var opacity: Double { disabled ? 0.5 : 1 }

@ptit-xav it didnt work.

i have another class that contains the ToolbarItemProperties

final class AppData: ObservableObject {
  @Published var toolbarItem1: ToolbarItemProperties?
}

it works if i do it like this

let item = ToolbarItemProperties()
item.disabled = true

appData.toolbarItem1 = item

but if i directly change the value of the property like this, nothing happens. it does not re-render. any idea why?

appData.toolbarItem1?.disabled = true

although if i do it like this (after setting a value to the disabled property, it works

appData.toolbarItem1 = appData.toolbarItem1

I still think even if a property of an ObservableObject is changed, it should refresh the view. not when the whole object is a set a value.

In your first example your properties are missing the @Published annotation. Views observing an ObservableObject only refresh on change of properties which are marked as @Published.

  • I also tried that. but nothing happens still.

Add a Comment

The issue you have is that ToolbarItemProperties is a class, and an instance of that class is stored in an @Published variable inside of an ObservableObject class. This AppData class is expected to watch the toolbarItem1 property for any changes (and then update the UI) but, because of the nature of a class, the difference between one before and after a variable change isn't as easily noticeable. If ToolbarItemProperties was a struct this wouldn't be a problem.

Because of this, you will need to manually publish changes to tell SwiftUI in advance that a variable is going to change and to reload the UI. This is done by using the provided property of the ObservableObject protocol:

objectWillChange.send()



Since you didn't provide any example code, I have created some which shows this working:

final class ToolbarItemProperties {
    var visible = false
    var disabled = false
    var icon = "questionmark"
    var action = {}

    var opacity: Double {
        disabled ? 0.5 : 1
    }
}

final class AppData: ObservableObject {
    @Published var toolbarItem1: ToolbarItemProperties?
}

struct ContentView: View {
    @StateObject private var appData: AppData

    init() {
        let appData = AppData()
        appData.toolbarItem1 = ToolbarItemProperties()
        _appData = StateObject(wrappedValue: appData)
    }

    var body: some View {
        VStack(spacing: 20) {
            Text("Hello, World!")
                .opacity(appData.toolbarItem1?.opacity ?? 1)

            let toggleBinding = Binding<Bool> {
                appData.toolbarItem1?.disabled == true
            } set: {
                // This line is the key part
                // Must be before the variable change
                appData.objectWillChange.send()
                
                appData.toolbarItem1?.disabled = $0
            }

            Toggle("Is Disabled", isOn: toggleBinding)
                .toggleStyle(.button)
        }
    }
}
  • ill get back to you in a few days since im out of town now. i actually also tried the manual trigger to no avail but it looks like the way you did it is different from what i did.

    ill check this one out and compare. thank you.

Add a Comment

@BabyJ hi. tried it and it works. but i only used this section here

// This line is the key part
// Must be before the variable change
appData.objectWillChange.send()

I ran that code after every property change e.g.

appData.toolbarItem1?.disabled = true
appData.toolbarItem1?.objectWillChange.send()

but when i placed that before as you mentioned, it worked. and i do not understand why they have it behave that way if send() is called before a property change.

ps tollbarItem1 is also an ObservableObject in the appData class.