I am using a GeometryReader to find the size of a view and want to report this to a preference key. The geometry reader always reads the correct value, but onPreferenceChanged often gets 0.0 reported instead of the value that was read.
I have simplified the case to illustrate the error. (Never mind that the size is already hard coded and I have no further use of the value in this example.)
struct HeightPreferenceKey: PreferenceKey {
static var defaultValue: CGFloat = 0
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = nextValue()
}
}
struct ContentView: View {
var body: some View {
ZStack {
Color.yellow
// Content to be inserted here...
}
.frame(width: 300, height: 300)
.background(
GeometryReader { proxy in
let _ = print("Setting preference key:", proxy.size.height)
Color.clear
.preference(key: HeightPreferenceKey.self, value: proxy.size.height)
}
)
.onPreferenceChange(HeightPreferenceKey.self) { height in
print("onPreferenceChange height: \(height)")
}
}
}
With the following content inserted, I would get this printout, which is to be expected:
Setting preference key: 300.0
onPreferenceChanged height: 300.0
Button {} label: { Image(systemName: "book") }
.buttonStyle(.borderless)
Text("Hello")
However, if I remove the button style or insert a condition, then onPreferenceChanged will only get 0.0
, as in these cases:
Button {} label: { Image(systemName: "book") }
if true {
Text("Hello")
}
Can anyone explain why this is happening, and how I can avoid this problem?
Your PreferenceKey
is implemented incorrectly. From the documentation for defaultValue
Views that have no explicit value for the key produce this default value. Combining child views may remove an implicit value produced by using the default. This means that
reduce(value: &x, nextValue: {defaultValue})
shouldn’t change the meaning ofx
.
Your PreferenceKey
does not satisfy this requirement. Reducing the default value 0
into 300
will change the value to 0
, and this is exactly what's happening in your code. Try putting some print
s in reduce
.
The idea for this requirement is that SwiftUI reserves the right to still call reduce
to combine child views even if they do not have a .preference
. This all depends on implementation details, and it is not strange that slight changes in the view hierarchy changes whether or not SwiftUI calls reduce
this way.
Notice that your onPreferenceChange
is put on a view that has multiple children other than the background
with a preference set - Color.yellow
, the button, the text, etc. It wouldn't be strange if SwiftUI tries to combine one of these children's preferences (the default value 0
) with the preference of the background
using your reduce
method.
If you put onPreferenceChange
immediately after .preference
, this is a lot less likely to happen.
Here is a correct implementation of a preference key, using an optional.
struct HeightPreferenceKey: PreferenceKey {
static let defaultValue: CGFloat? = nil
static func reduce(value: inout CGFloat?, nextValue: () -> CGFloat?) {
if let next = nextValue() {
value = next
}
}
}