SwiftUI: Sending preference key when publisher/binding emits value

Viewed 123

I have a situation where I have a view model with a @Published value. Now when I'm building the UI I want to send a value via a preference every time the published value changes. The problem I'm having is that I cannot figure out how to get SwiftUI to watch the published value and send the preference.

In the code below I've endeavoured to simplify the problem.

Preference key:

struct NewValueKey: PreferenceKey {
    static var defaultValue: String? = nil
    static func reduce(value: inout String?, nextValue: () -> String) {
        value = nextValue()
    }
}

Publishing object:

public class SomeViewModel: ObservableObject {
    @Published public var value: String?
}

And in the Swift UI view:

.onReceive(viewModel.$value) { _ in
    let newValue = "abc"
    Color.clear.preference(key: NewValueKey.self, value: newValue)
}

But I get a variety of errors such as the result of the preference not being used, etc. The problem is that I need to execute some code to build the value I want to send so I can't just forward the value I receive. I've also looked at using a @State property as some sort of intermediary but I'm not sure how to wire it all together.

Any suggestions?

2 Answers

It is enough to inject value directly into preference modifier attached to a view within modified context (ie. you don't need .onReceive modifer because updated value will activate .preference modifier directly).

Tested with Xcode 13.3 / iOS 15.4

Here is a snapshot of main part

Button("Generate") {
    vm.value = String(Int.random(in: 0...9))
}
.preference(key: NewValueKey.self, value: vm.value)  // << here !!

Completed findings report and code is here

not clear what you want, but ensure that you have something like this in your view, or equivalent @ObservedObject:

    @StateObject var viewModel = SomeViewModel()

Also:

struct NewValueKey: PreferenceKey {
    static var defaultValue: String = ""  // <-- here

    static func reduce(value: inout String, nextValue: () -> String) { // <-- here
        value = nextValue()
    }
}

and to get rid of the warning:

    .onReceive(viewModel.$value) { _ in
        let newValue = "abc"
        let _ = Color.clear.preference(key: NewValueKey.self, value: newValue) // <-- here
    }
    

or

    .onReceive(viewModel.$value) { val in
        if let newValue = val {
            let _ = Color.clear.preference(key: NewValueKey.self, value: newValue) // <-- here
        }
    }        
Related