SwiftUI中根据内容最大高度动态设置水平ScrollView高度
SwiftUI 水平轮播组件:让ScrollView高度匹配内容
问题描述
已实现基于SwiftUI ScrollView的水平轮播组件,功能正常,但添加背景色后发现ScrollView占用了额外垂直空间,希望让ScrollView高度自动匹配内容高度,避免多余空白区域。
原轮播组件代码
struct CarouselView<Content: View>: View { let content: Content @State private var currentIndex = 0 @State private var contentSize: CGSize = .zero private var showsIndicators: Bool private var spacing: CGFloat private var offset: CGFloat private var shouldSnap: Bool init(showsIndicators: Bool = true, spacing: CGFloat = 0, offset: CGFloat = 0, shouldSnap: Bool = false, @ViewBuilder content: @escaping () -> Content) { self.content = content() self.showsIndicators = showsIndicators self.spacing = spacing self.offset = offset self.shouldSnap = shouldSnap } var body: some View { ScrollView(.horizontal, showsIndicators: showsIndicators) { LazyHStack(spacing: spacing) { content .offset(x: offset) }.apply { if #available(iOS 17.0, *), shouldSnap { $0.scrollTargetLayout() } else { $0 } } } .apply { if #available(iOS 17.0, *), shouldSnap { $0.scrollTargetBehavior(.viewAligned) } else { $0 } } } } extension View { func apply<V: View>(@ViewBuilder _ block: (Self) -> V) -> V { block(self) } }
原使用代码
struct ContentView: View { let imagesNames: [String] = ["img-1", "img-2", "img-3"] var body: some View { VStack(spacing: 10, content: { pagingCarousel .background(.yellow) continuousCarousel .background(.green) }) } private var pagingCarousel: some View { CarouselView(showsIndicators: false, spacing: 10, shouldSnap: true) { ForEach(imagesNames, id: \.self) { image in createTile(with: image) } } } private var continuousCarousel: some View { CarouselView(showsIndicators: true, spacing: 20, offset: 20) { ForEach(imagesNames, id: \.self) { image in createTile(with: image) } } } private func createTile(with image: String) -> some View { ZStack { Image(image) .resizable() .scaledToFill() .overlay(Color.black.opacity(0.5)) Text("Headline") .bold() .foregroundStyle(.white) } .frame(height: 200) .cornerRadius(30) .clipped() } }
解决方案
核心思路是获取内容的实际高度,将ScrollView的高度强制设置为该值,具体修改如下:
- 给轮播内容添加
GeometryReader背景,实时读取内容尺寸并更新状态变量 - 给ScrollView绑定该尺寸,限制其高度与内容高度一致
修改后的完整CarouselView代码:
struct CarouselView<Content: View>: View { let content: Content @State private var currentIndex = 0 @State private var contentSize: CGSize = .zero private var showsIndicators: Bool private var spacing: CGFloat private var offset: CGFloat private var shouldSnap: Bool init(showsIndicators: Bool = true, spacing: CGFloat = 0, offset: CGFloat = 0, shouldSnap: Bool = false, @ViewBuilder content: @escaping () -> Content) { self.content = content() self.showsIndicators = showsIndicators self.spacing = spacing self.offset = offset self.shouldSnap = shouldSnap } var body: some View { ScrollView(.horizontal, showsIndicators: showsIndicators) { LazyHStack(spacing: spacing) { content .offset(x: offset) // 通过GeometryReader获取内容实际尺寸 .background(GeometryReader { proxy in Color.clear .onAppear { contentSize = proxy.size } .onChange(of: proxy.size) { newSize in contentSize = newSize } }) }.apply { if #available(iOS 17.0, *), shouldSnap { $0.scrollTargetLayout() } else { $0 } } } // 强制ScrollView高度匹配内容高度 .frame(height: contentSize.height) .apply { if #available(iOS 17.0, *), shouldSnap { $0.scrollTargetBehavior(.viewAligned) } else { $0 } } } } extension View { func apply<V: View>(@ViewBuilder _ block: (Self) -> V) -> V { block(self) } }
说明
GeometryReader会读取包裹内容的真实尺寸,通过onAppear和onChange实时更新contentSize- 给ScrollView设置
frame(height: contentSize.height)后,ScrollView会严格按照内容高度布局,不再占用额外垂直空间 - 原有轮播的滚动、分页等功能不受任何影响
内容的提问来源于stack exchange,提问作者Shawn Frank
相关产品推荐
相关产品推荐

