SwiftUI NavigationStack父视图尺寸变化时无法自适应匹配问题
问题现象
在自适应内容尺寸的Sheet中使用NavigationStack导航时,Sheet内容尺寸变化后,NavigationStack无法通过PreferenceKeys自适应新尺寸,导致栈内内容被截断。尝试调整NavigationStack的.frame()属性时遇到以下问题:
.frame(maxHeight: .infinity):无任何效果.frame(height: sheetHeight):看似可行,但尺寸并非总能更新.frame(height: 1000):可解决截断问题,但第三个导航页面中的文本字段编辑时键盘无法正常工作
问题原因
这并非严格意义上的Bug,而是NavigationStack在Sheet环境中,布局优先级与Sheet的presentation detents机制存在冲突:
- PreferenceKey获取的视图高度与Sheet的detent高度形成循环依赖,导致尺寸更新不及时
NavigationStack自身的布局容器在Sheet中未正确响应外部尺寸变化
修复方案
- 移除每个视图中冗余的
sheetHeightSaved状态,直接基于当前视图真实内容高度计算Sheet的detent高度 - 给
NavigationStack绑定最大高度约束,使其填充Sheet可用空间 - 优化PreferenceKey的使用逻辑,确保仅在视图内容变化时更新Sheet高度
- 针对键盘弹出场景,添加键盘监听动态调整Sheet高度,解决第三个页面的键盘适配问题
修改后的完整代码
import SwiftUI struct ContentView: View { @State var sheetHeight: CGFloat = 100 @State var sheetIsPresented = false var body: some View { VStack { Button(action: { sheetIsPresented.toggle() sheetHeight = 100 }) { Text("显示Sheet") } } .sheet(isPresented: $sheetIsPresented) { SheetNavStack(sheetHeight: $sheetHeight, sheetIsPresented: $sheetIsPresented) .presentationDetents([.height(sheetHeight), .large]) .presentationDragIndicator(.hidden) .presentationBackgroundInteraction(.enabled) .interactiveDismissDisabled() .frame(maxHeight: .infinity) } } } struct SheetNavStack: View { @State var path = NavigationPath() @Binding var sheetHeight: CGFloat @Binding var sheetIsPresented: Bool var body: some View { NavigationStack(path: $path) { NavViewOne(path: $path, sheetHeight: $sheetHeight, sheetIsPresented: $sheetIsPresented) .navigationDestination(for: NavViewDestinationTwo.self) { _ in NavViewTwo(path: $path, sheetHeight: $sheetHeight) } .navigationDestination(for: NavViewDestinationThree.self) { _ in NavViewThree(path: $path, sheetHeight: $sheetHeight, sheetIsPresented: $sheetIsPresented) } } .frame(maxHeight: .infinity) } } struct NavViewOne: View { @Binding var path: NavigationPath @Binding var sheetHeight: CGFloat @Binding var sheetIsPresented: Bool var body: some View { VStack { Text("页面一") Text("页面一") Text("页面一") HStack { Button(action: { sheetIsPresented = false }) { Text("返回") } Spacer() Button(action: { path.append(NavViewDestinationTwo()) }) { Text("前进") } } } .navigationBarBackButtonHidden(true) .padding() .background( GeometryReader { geometry in Color.clear.preference(key: InnerHeightPreferenceKey.self, value: geometry.size.height) } ) .onPreferenceChange(InnerHeightPreferenceKey.self) { newHeight in withAnimation { sheetHeight = newHeight + 20 } } } } struct NavViewTwo: View { @Binding var path: NavigationPath @Binding var sheetHeight: CGFloat var body: some View { VStack { Text("页面二") Text("页面二") Text("页面二") Text("页面二") Text("页面二") Text("页面二") HStack { Button(action: { path.removeLast(1) }) { Text("返回") } Spacer() Button(action: { path.append(NavViewDestinationThree()) }) { Text("前进") } } } .navigationBarBackButtonHidden(true) .padding() .background( GeometryReader { geometry in Color.clear.preference(key: InnerHeightPreferenceKey.self, value: geometry.size.height) } ) .onPreferenceChange(InnerHeightPreferenceKey.self) { newHeight in withAnimation { sheetHeight = newHeight + 20 } } } } struct NavViewThree: View { @Binding var path: NavigationPath @Binding var sheetHeight: CGFloat @Binding var sheetIsPresented: Bool @State var textInput: String = "" @State var textEditorInput: String = "" @State private var keyboardHeight: CGFloat = 0 var body: some View { VStack { Text("页面三") Text("页面三") Text("页面三") Text("页面三") TextField("提示", text: $textInput) .textFieldStyle(RoundedBorderTextFieldStyle()) TextEditor(text: $textEditorInput) .frame(height: 200) .scrollContentBackground(.hidden) .background(RoundedRectangle(cornerRadius: 5).foregroundColor(.gray)) HStack { Button(action: { path.removeLast(1) }) { Text("返回") } Spacer() Button(action: { sheetIsPresented = false }) { Text("完成") } } } .navigationBarBackButtonHidden(true) .padding() .background( GeometryReader { geometry in Color.clear.preference(key: InnerHeightPreferenceKey.self, value: geometry.size.height + keyboardHeight) } ) .onPreferenceChange(InnerHeightPreferenceKey.self) { newHeight in withAnimation { sheetHeight = newHeight + 20 } } .onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillShowNotification)) { notification in guard let frame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { return } keyboardHeight = frame.height } .onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillHideNotification)) { _ in keyboardHeight = 0 } } } struct InnerHeightPreferenceKey: PreferenceKey { static var defaultValue: CGFloat = .zero static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { value = nextValue() } } struct NavViewDestinationOne: Hashable {} struct NavViewDestinationTwo: Hashable {} struct NavViewDestinationThree: Hashable {}
关键修复点说明
- 给
SheetNavStack和NavigationStack添加.frame(maxHeight: .infinity),确保它们能填充Sheet的可用空间 - 移除冗余的
sheetHeightSaved状态,避免不必要的状态同步冲突 - 在第三个页面中添加键盘监听,动态调整Sheet高度,解决键盘弹出时的布局适配问题
- 给计算出的视图高度添加额外边距,提升视觉体验
内容的提问来源于stack exchange,提问作者Angus
相关产品推荐
相关产品推荐

