Here is my code for learning PreferenceKey. I’m trying to update title from child view and showing in parent view via extensions and modifiers but onPreferenceChange is not getting called.
MainView
struct MainView: View {
@State var title: String = "DEF"
var body: some View {
VStack {
Text(title)
.padding()
.font(.system(size: 20))
.foregroundStyle(Color.black)
.titleByKey(title: $title)
ChildView()
}
}
}
struct ChildView: View {
@State var title: String = ""
var body: some View {
TextField("Enter", text: $title)
.padding()
.font(.system(size: 20))
.foregroundStyle(Color.black)
.background(Color.gray)
.updateTitleByKey(string: title)
}
}
Custom PreferenceKey
import Foundation
import SwiftUI
struct TitleChangePreferenceKey: PreferenceKey {
static var defaultValue: String = ""
static func reduce(value: inout String, nextValue: () -> String) {
value = nextValue()
}
}
extension View {
func updateTitleByKey(string: String) -> some View {
print("UP 1", string)
return preference(key: TitleChangePreferenceKey.self, value: string)
}
func titleByKey(title: Binding<String>) -> some View {
print("UP 2", title)
return modifier(TitleChangeModifier(title: title))
}
}
struct TitleChangeModifier: ViewModifier {
@Binding var title: String
func body(content: Content) -> some View {
content
.onChange(of: title) { _, value in
print("onChange", value)
}
.onPreferenceChange(TitleChangePreferenceKey.self) { value in
print("onPreferenceChange", value)
title = value
}
}
}
But neither onPreferenceChange nor onChange is called. Let me know what i missed?
>Solution :
I assume that the purpose of using preferences here is only for learning about preference keys. If this is real code, you should just pass a Binding directly to ChildView, and not have such a convoluted approach.
Preferences propagate up the view hierarchy, not across siblings. .titleByKey(title: $title) would not detect any change if it is placed on the Text. Text has no preference.
You should place it on the ChildView, since the ChildView is the one that has the preference (set by updateTitleByKey).
var body: some View {
VStack {
Text(title)
.padding()
.font(.system(size: 20))
.foregroundStyle(Color.black)
ChildView()
.titleByKey(title: $title)
}
}
It also works if you put .titleByKey(title: $title) on the VStack, since it is the parent of ChildView.
Side note: I would implement the preference key with a default value of nil:
struct TitleChangePreferenceKey: PreferenceKey {
static var defaultValue: String? { nil }
static func reduce(value: inout String?, nextValue: () -> String?) {
guard let next = nextValue() else { return }
value = next
}
}
.onPreferenceChange(TitleChangePreferenceKey.self) { value in
guard let value else { return } // added
print("onPreferenceChange", value)
title = value
}
This is because the contract of defaultValue is that reduce(value: &x, nextValue: {defaultValue}) should not change x.