You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

SwiftUI NavigationStack父视图尺寸变化时无法自适应匹配问题

自适应Sheet中NavigationStack内容截断问题修复

问题现象

在自适应内容尺寸的Sheet中使用NavigationStack导航时,Sheet内容尺寸变化后,NavigationStack无法通过PreferenceKeys自适应新尺寸,导致栈内内容被截断。尝试调整NavigationStack的.frame()属性时遇到以下问题:

  • .frame(maxHeight: .infinity):无任何效果
  • .frame(height: sheetHeight):看似可行,但尺寸并非总能更新
  • .frame(height: 1000):可解决截断问题,但第三个导航页面中的文本字段编辑时键盘无法正常工作

问题原因

这并非严格意义上的Bug,而是NavigationStack在Sheet环境中,布局优先级与Sheet的presentation detents机制存在冲突:

  1. PreferenceKey获取的视图高度与Sheet的detent高度形成循环依赖,导致尺寸更新不及时
  2. NavigationStack自身的布局容器在Sheet中未正确响应外部尺寸变化

修复方案

  1. 移除每个视图中冗余的sheetHeightSaved状态,直接基于当前视图真实内容高度计算Sheet的detent高度
  2. 给NavigationStack绑定最大高度约束,使其填充Sheet可用空间
  3. 优化PreferenceKey的使用逻辑,确保仅在视图内容变化时更新Sheet高度
  4. 针对键盘弹出场景,添加键盘监听动态调整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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.09 11:39:54