UPGRADE YOUR SKILLS: Learn advanced Swift and SwiftUI on Hacking with Swift+! >>

Is there a way to trigger a function call when certain properties change, without appending a didSet to each of them?

Forums > SwiftUI

Short example, in reality there are many more properties in my use case:

class MyObservable: ObservableObject {
    @Published var firstProperty = false
    @Published var secondProperty = 3
    @Published var thirdProperty = "Hello"
    // ...

    func doThisWhenCertainPropertiesChange() {
        // ...
    }
}

Now I could append { didSet { doThisWhenCertainPropertiesChange() } } to each of the properties which, when changed, should trigger the function, but I'm wondering if there exists a more elegant and less repetitive way.

I was wondering if there's maybe a way that MyObservable's objectWillChange publisher could send which property triggered a send() (maybe be sending as its value the key path to the property)?

3      

I don't think you can avoid writing a bunch of didSet blocks. Sometimes you gotta boil the plates when the plates need boiling. But, I did manage to have fun coming up with a really hacky solution to send the property through to the function when it's called. Just give them to the function as arguments. Originally I tried to get the variable name using Mirror but I gave up on that.

class MyObservable: ObservableObject {
    @Published var firstProperty = false {
        didSet{
            doThisWhenCertainPropertiesChange(property: firstProperty, senderName: "firstProperty")
        }
    }
    @Published var secondProperty = 3 {
        didSet{
            doThisWhenCertainPropertiesChange(property: secondProperty, senderName: "secondProperty")
        }
    }
    @Published var thirdProperty = "string" {
        didSet{
            doThisWhenCertainPropertiesChange(property: thirdProperty, senderName: "thirdProperty")
        }
    }
    // ...

    func doThisWhenCertainPropertiesChange(property:Any, senderName: String) {
        // ...
        print("sender name: \(senderName)")
        print("type: \(type(of: property))")
        print("value: \(property)")
    }
}

let james = MyObservable()

james.firstProperty = true
james.secondProperty = 5
james.thirdProperty = "changed"

3      

In case you can use didSet: you can use ReferenceWritableKeyPath as non-stringly typed property reference and make doThisWhenCertainPropertiesChange use a generic type T and therefore avoid type casting when you want to access it:

class MyObservable: ObservableObject {
    @Published var firstProperty = false {
        didSet{
            doThisWhenCertainPropertiesChange(property: firstProperty, sender: \.firstProperty)
        }
    }

...

    func doThisWhenCertainPropertiesChange<T>(
        property: T, sender: ReferenceWritableKeyPath<MyObservable, T>
    ) {
        if sender == \.firstProperty {
            print("firstProperty: ", property)
        } else if sender == \.secondProperty {
            print("secondProperty: ", property)
        } else if sender == \.thirdProperty {
            print("thirdProperty: ", property)
        }
    }

I thought it would be good if you had your own version of @Published which sends the "future value" and the property along with the notification. The default objectWillChange publisher is always sending "nothing" as its value. Replacing it with the actual value and the ReferenceWritableKeyPath to the property would be a way to go.

But I've never tackled to write a property wrapper which accesses "something" from the parent. I think this should be possible since Swift 5.4 and was not possible at the time of iOS 14. This is why @AppStorage and @SceneStorage are always taking a stringly typed name and cannot infer a generic name derived from the property.

Perhaps this article from John Sundell might be a path to implement a custom @Published property wrapper: https://www.swiftbysundell.com/articles/accessing-a-swift-property-wrappers-enclosing-instance/

3      

I would probably rethink the design of that ObservableObject if I found it having so many properties and having to write so much boilerplate code.

3      

If you need to react to a change in a property in a specific View, wouldn't it be better to use the .onChange() modifier or am I misunderstanding the point of your question?

I guess where I am coming from is if the user does something that affects a property then that would be occuring in a View so that's why my suggestion is to use the .onChange() modifier since the Published property would be accessible from within the View via your ViewModel ObservableObject.

3      

Thanks for all the suggestions. The function doThisWhenCertainPropertiesChange doesn't need the value of the property that triggers it when changed, it just needs to get triggered by only a certain subset of the total properties on MyObservable.

I.e. "If one of these 11 properties changes (but not if one of the remaining 13 properties changes), call doThisWhenCertainPropertiesChange()!"

This maybe also better illustrates why onChange(of:perform:) doesn't work here since I'd need one of those modifiers for each of the 11 properties (each calling the same function).

However, the properties are not homogeneous in a way where it would be more appropriate to collect them in a separate ObservableObject for which then any change would trigger doThisWhenCertainPropertiesChange. All the properties in question do need to stay on MyObservable.

I had already solved this with a custom property wrapper that replaces @Published – actually @AppStorage in my case – but was wondering if there's another way. I suppose a custom property wrapper @MyCustomAppStorage with an

init(wrappedValue: Value,
     _ key: String,
     store: UserDefaults? = nil,
     callDoThisWhenCertainPropertiesChange: Bool = false)

is maybe the best way to achieve this at the moment.

3      

BUILD THE ULTIMATE PORTFOLIO APP Most Swift tutorials help you solve one specific problem, but in my Ultimate Portfolio App series I show you how to get all the best practices into a single app: architecture, testing, performance, accessibility, localization, project organization, and so much more, all while building a SwiftUI app that works on iOS, macOS and watchOS.

Get it on Hacking with Swift+

Sponsor Hacking with Swift and reach the world's largest Swift community!

Archived topic

This topic has been closed due to inactivity, so you can't reply. Please create a new topic if you need to.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.