iOS不同版本下GeometryReader计算Sheet高度结果不一致求助
修复iOS 18中Sheet内GeometryReader高度为0的问题,实现自适应高度不可拖拽Sheet
问题概述
需求:在iOS 16及以上版本中,使用presentationDetent(.height)结合GeometryReader计算内容高度,实现Sheet自适应内容,且第二个Sheet不可拖拽、保持计算后的固定尺寸。
问题:GeometryReader返回的高度存在版本差异——iPhone 15模拟器iOS 17.4环境下返回正确高度,iOS 18环境下返回0.0。
已尝试:Preference Key、将overlay替换为background、用task替代onAppear,均未解决。
核心原因
iOS 18调整了Sheet的布局触发时机:onAppear在Sheet内容尚未完成布局时就会触发,导致GeometryReader无法获取有效尺寸;而旧版本中onAppear触发时机晚于内容布局,能拿到正确高度。
可行修复方案
方案1:PreferenceKey结合异步更新布局
通过PreferenceKey捕获内容实际高度,在布局完成后异步更新Sheet的detent,同时隐藏拖拽指示器实现不可拖拽。
步骤1:定义PreferenceKey
struct ContentHeightPreferenceKey: PreferenceKey { static var defaultValue: CGFloat = 0 static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { value = nextValue() } }
步骤2:修改Sheet实现
struct ContentView: View { @ObservedObject var viewModel: ViewModel @State var sheetHeight: CGFloat = .zero @State var isSheetLayoutComplete = false // 标记内容布局是否完成 var body: some View { Text("Parent view") .sheet(isPresented: isPresented(for: .sheetOne), content: { VStack { Text("Sheet one") Button(action: { viewModel.activeSheet = .sheetTwo}, label: { Text("Next")}) } }) .sheet(isPresented: isPresented(for: .sheetTwo), content: { VStack(spacing: 0) { Text("Sheet two") Text("Sheet two") } // 用background捕获内容尺寸,确保优先级低于内容布局 .background { GeometryReader { proxy in Color.clear .preference(key: ContentHeightPreferenceKey.self, value: proxy.size.height) } } // 监听高度变化,异步更新避免布局循环 .onPreferenceChange(ContentHeightPreferenceKey.self) { newHeight in guard newHeight > 0 else { return } sheetHeight = newHeight DispatchQueue.main.async { isSheetLayoutComplete = true } } // 初始用.automatic让内容正常布局,完成后切换到固定高度 .presentationDetents(isSheetLayoutComplete ? [.height(sheetHeight)] : [.automatic]) // 隐藏拖拽指示器,禁止拖拽调整尺寸 .presentationDragIndicator(.hidden) }) } }
方案2:使用自定义Detent(iOS 16.4+)
利用.custom detent直接在回调中返回计算后的高度,无需依赖额外状态标记,实现更简洁:
struct ContentView: View { @ObservedObject var viewModel: ViewModel @State var sheetHeight: CGFloat = .zero var body: some View { Text("Parent view") .sheet(isPresented: isPresented(for: .sheetOne), content: { VStack { Text("Sheet one") Button(action: { viewModel.activeSheet = .sheetTwo}, label: { Text("Next")}) } }) .sheet(isPresented: isPresented(for: .sheetTwo), content: { VStack(spacing: 0) { Text("Sheet two") Text("Sheet two") } .background { GeometryReader { proxy in Color.clear .onAppear { sheetHeight = proxy.size.height } } } // 自定义detent,初始用临时高度避免0尺寸显示 .presentationDetents([.custom { _ in sheetHeight == 0 ? 100 : sheetHeight }]) .presentationDragIndicator(.hidden) }) } }
关键注意点
- 优先用
background替代overlay:background的布局优先级低于内容,能确保GeometryReader捕获的是内容实际渲染尺寸。 - 异步更新状态:避免直接在布局回调中修改detent触发布局循环。
- 隐藏拖拽指示器+单一detent:两者结合可彻底禁止Sheet的拖拽调整行为。
内容的提问来源于stack exchange,提问作者victorgson
相关产品推荐
相关产品推荐

