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

SwiftUI:在ScrollView内嵌VStack时如何获取真实内容高度?

问题:ScrollView内如何获取VStack的真实内容高度?

我用以下代码尝试获取内容视图的高度:

.background {
    GeometryReader { geo in
        Color.red.onAppear {
            print(geo.size.height)
            contentHeight = geo.size.height
        }
    }
}

但计算结果几乎是全屏高度(比如得到943.4399999999999),而实际只渲染了2个高度为50px的SProjectItem,红色背景区域也远小于这个值。

完整代码如下:

NavigationStack(path:$store.navigationPath.sending(\.setNavigationPath)) {
    ScrollView(.vertical){
        VStack(spacing:16) {
            ForEach(0..<2, id: \.self) { index in
                SProjectItem()
                    .foregroundStyle(Color.theme.foreground)
            }
        }
        .background {
            GeometryReader { geo in
                Color.red.onAppear {
                    contentHeight = geo.size.height
                }
            }
        }
    }
    .navigationDestination(for: Project.self) { project in
        SFoldersScreen()
            .toolbar(.hidden)
    }
}

更新:移除ScrollView后,VStack背景的GeometryReader能正确计算内容高度,但需要保留ScrollView的情况下解决这个问题。


解决方法:利用Preference捕获真实高度

ScrollView会允许子视图占据可用的最大空间,直接用GeometryReader读取的是ScrollView的可用高度,而非VStack的实际内容高度。可以通过PreferenceKey机制捕获视图的真实intrinsic高度:

方法1:直接使用PreferenceKey

  1. 定义一个PreferenceKey:
struct ContentHeightPreferenceKey: PreferenceKey {
    static var defaultValue: CGFloat = 0
    static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
        value = nextValue()
    }
}
  1. 修改原代码,通过preference传递高度:
NavigationStack(path:$store.navigationPath.sending(\.setNavigationPath)) {
    ScrollView(.vertical){
        VStack(spacing:16) {
            ForEach(0..<2, id: \.self) { index in
                SProjectItem()
                    .foregroundStyle(Color.theme.foreground)
            }
        }
        .background {
            GeometryReader { geo in
                Color.red
                    .preference(key: ContentHeightPreferenceKey.self, value: geo.size.height)
            }
        }
        .onPreferenceChange(ContentHeightPreferenceKey.self) { newHeight in
            contentHeight = newHeight
            print("真实内容高度:\(newHeight)")
        }
    }
    .navigationDestination(for: Project.self) { project in
        SFoldersScreen()
            .toolbar(.hidden)
    }
}

方法2:封装自定义视图复用逻辑

如果需要多次使用,可以封装一个IntrinsicSizeReader视图:

struct IntrinsicSizeReader<Content: View>: View {
    private let content: (CGSize) -> Content
    @Binding var size: CGSize
    
    init(size: Binding<CGSize>, @ViewBuilder content: @escaping (CGSize) -> Content) {
        self._size = size
        self.content = content
    }
    
    var body: some View {
        content(size)
            .background {
                GeometryReader { geo in
                    Color.clear
                        .preference(key: SizePreferenceKey.self, value: geo.size)
                }
            }
            .onPreferenceChange(SizePreferenceKey.self) { newSize in
                size = newSize
            }
    }
    
    private struct SizePreferenceKey: PreferenceKey {
        static var defaultValue: CGSize = .zero
        static func reduce(value: inout CGSize, nextValue: () -> CGSize) {
            value = nextValue()
        }
    }
}

使用时直接包裹目标VStack:

ScrollView(.vertical){
    IntrinsicSizeReader(size: $contentSize) { _ in
        VStack(spacing:16) {
            ForEach(0..<2, id: \.self) { index in
                SProjectItem()
                    .foregroundStyle(Color.theme.foreground)
            }
        }
        .background(Color.red)
    }
}
// 此时contentSize.height就是VStack的真实高度

原理说明:Preference机制可以在视图树中传递值,通过GeometryReader捕获VStack的实际渲染高度后,传递给父视图的绑定变量,不受ScrollView的布局扩展影响。


内容的提问来源于stack exchange,提问作者MiguelG

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.14 06:02:43