Post

Replies

Boosts

Views

Activity

TextField appears to break preferences
I've written a view modifier that relies on the preference system to wrap a view in a ScrollView if it overflows the space it's allowed onscreen struct ScrollableOverflow: ViewModifier { // MARK: State Properties /// Indicates if the content takes more vertical space than is available @State private var isOverflowing = false /// The vertical height of the content in pixels @State private var childHeight: CGFloat? = nil // MARK: Instance Properties /** The position of the content on the horizontal axis Since `GeometryReader` takes up all the space it's offered and always left-aligns its conent, this is necessary to apply proper horizontal positioning to the rendered content */ let alignment: HorizontalAlignment // MARK: Initializers /** Creates a new `ScrollableOverflow` modifier - Parameter alignment: The position of the content on the horizontal axis */ init(alignment: HorizontalAlignment = .center) { self.alignment = alignment } // MARK: ViewModifier Conformance func body(content: Content) -> some View { GeometryReader { availableGeometry in HStack { if alignment == .center || alignment == .trailing { Spacer() } content .fixedSize(horizontal: false, vertical: true) .background( GeometryReader { contentGeometry in Color.clear .preference(key: ScrollableChildViewHeight.self, value: dump(contentGeometry.size.height)) } ) .onPreferenceChange(ScrollableChildViewHeight.self) { newChildHeight in print("Modifier height: \(newChildHeight)") childHeight = newChildHeight isOverflowing = newChildHeight > availableGeometry.size.height } if alignment == .center || alignment == .trailing { Spacer() } } .scroll(when: isOverflowing) } .frame(maxHeight: childHeight) } // MARK: Helper Types /// The preference key used to communicate the rendered height of the child view of a `ScrollableOverflow` view modifier back up to its parent to know if a `ScrollView` needs to be rendered private struct ScrollableChildViewHeight: PreferenceKey { static let defaultValue: CGFloat = 0 static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { value = nextValue() } } } // MARK: - View Extension extension View { /** Renders content in a scrollable way only if it overflows the vertical space it's allowed - Parameter alignment: The position of the content on the horizontal axis */ func scrollOnOverflow(alignment: HorizontalAlignment = .center) -> some View { modifier(ScrollableOverflow(alignment: alignment)) } /** Makes a view scrollable only when a condition applies - Parameter condition: Determines if the view should be scrollable */ @ViewBuilder fileprivate func scroll(when condition: Bool) -> some View { if condition { ScrollView { self } } else { self } } } This works for most views, but when I add a TextField to the hierarchy that overflows, the outer GeometryReader of the modifier ends up always getting its height set to 0 let designedLabel = Text("Email Address or Phone Number") .font(.headline) VStack(alignment: .leading, spacing: 20) { Text("Please fill in whichever is connected to your account to receive a verification code") .fixedSize(horizontal: false, vertical: true) VStack(alignment: .leading) { #if !os(macOS) // Platforms like macOS display the label given to the TextField directly, but not iOS designedLabel .fixedSize(horizontal: false, vertical: true) #endif TextField(text: $text, prompt: Text("you@example.com / +1 (555)-867-5309")) { designedLabel } } } .padding() .background(Color.gray) .cornerRadius(20) .scrollOnOverflow() This seems to be because the ScrollableChildViewHeight preference always takes its defaultValue (which is 0, but you can verify this by changing it to whatever you'd like), and never gets updated with the correct value. However, if you look at the console with the dump and print statements that are there, you'll see that the preference does properly get set with what seems to be a correct value, though the call to onPreferenceChange never gets anything other than the defaultValue. Removing the TextField makes this system work seemingly flawlessly Am I missing something or misunderstanding how the preference system works, or is this a SwiftUI bug? I'm using the latest Xcode version 13.2.1 (13C100) on the latest macOS Monterey 12.2 and targeting iOS 15.2, and this seems to be the case on all the simulators and my physical iPhone 12 Pro Max
4
0
649
Feb ’22