Search code examples
swiftuigeometryreader

onPreferenceChange not called on geometryReader frame change


PreferenceKey definition

struct ScrollPrefKey: PreferenceKey {
  static var defaultValue: CGFloat = 0

  static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
    value = nextValue()
    
  }
}

Main Code

 ScrollView {
     GeometryReader { proxy in
                    
         Text("\(proxy.frame(in: .named("scroll")).minY)") // I can see changes
                        
         Color.clear.preference(key: ScrollPrefKey.self, value: proxy.frame(in: .named("scroll")).minY)
     } //MARK: END GeometryReader

     VStack {
       //main content
       Button(action: {}, label: {Text("myButton")})
     }

 }
 .coordinateSpace(name: "scroll")
 .onPreferenceChange(ScrollPrefKey.self, perform: { value in
    print(value)
    //DO SOME THINGS - but it never trigger
 })
  1. This code works in Preview but not using simulator (iOS 16.1)
  2. Replacing VStack with a static Color with defined height works
  3. Embedding the entire content of scrollView including GeometryReader still doesn't work

Solution

  • static func reduce(value: inout Self.Value, nextValue: () -> Self.Value)
    

    Here, nextValue: () -> Self.Value : This function allows for logic to reduce all those preferences to a single value, if multiple values are outputted by multiple different views, all using the same PreferenceKey.

    When the view loads first time, the preference key will be updated from defaultValue to initial value. And since we have only one view producing the value with one preference key, the nextValue() returns nil in this case.

    value: inout Self.Value: Though the value, which is keep accumulating through previous calls and keeps updating with the preference key value changes. So we should use the value here.

    Update the PreferenceKey definition in the code with this:

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