I’m struggling trying to implement a PreferenceKey. I’ve read countless articles, so
obviously I’m missing something. onPreferenceChange reports once at simulator startup but
not during scrolling. Scrolling updates the text in the TextEditor overlay, but the
offsetCGSize property is apparently never updated. Here’s a simplified version
of the code:
struct SOView: View {
@State private var firstText: String = "first"
@State private var secondText: String = "second"
@State private var thirdText: String = "third"
@State private var offsetCGSize: CGSize = .zero
var body: some View {
ScrollView {
VStack {
ForEach(0..<20) {index in
Text("Line number (index)")
}
Image(systemName: "globe")
.imageScale(.large)
.foregroundColor(.accentColor)
TextField("First", text: $firstText)
GeometryReader { teGeo in
TextEditor(text: $secondText)
.frame(height: 100)
.overlay(
RoundedRectangle(cornerRadius: 10).stroke(lineWidth: 2)
)
.overlay(
Text("(teGeo.frame(in: .global).minY)")
)
.preference(key: OffsetPreferenceKey.self, value: CGSize(width: 0, height: teGeo.frame(in: .global).minY))
}//geo
.frame(height: 100)
TextField("Third", text: $thirdText)
Text("TextEditor top is (offsetCGSize.height)")
}//v
.padding()
}//scroll
.onPreferenceChange(OffsetPreferenceKey.self) { value in
offsetCGSize = value
print("offsetCGSize is (offsetCGSize)")
}
}//body
}//so view
private struct OffsetPreferenceKey: PreferenceKey {
static var defaultValue: CGSize = CGSize.zero
static func reduce(value: inout CGSize, nextValue: () -> CGSize) {
value = nextValue()
}
}//off pref
Any guidance would be appreciated. Xcode 14.0.1, iOS 16
2
Answers
For others - I don't understand why, but by moving the two TextFields, the Image and the reporting Text outside of the ScrollView I get the results I expect. (In this file, I changed the key name to OffsetPreferenceKey2)
Like JohnSF, I have also been searching for use cases for preferenceKey. After a lot of searching and playing around, I wanted to post some information that hopefully will help others. Essentially there are 3 parts to a successful implementation. In my example, I needed to be able to react the any size change to a ScrollView (i.e. different screen resolutions, portrait vs landscape).
So, if you wanted to track just the size, replace all the CGRect with CGSize, etc.
Next, define an @State variable to store the values you want to track. In my case I used
@State private var rect: CGRect: .zero
So my changes I’m tracking will be found in my "rect" variable anytime I want them.
Lastly if there is a View that you want to track the size of like an image, you can place your preference key and a GeometryReader in the background like this.
Then put your .onPreferenceChange on something like a VStack, etc. Then if you rotate your device, you can read the new size.