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

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的高度强制设置为该值,具体修改如下:

  1. 给轮播内容添加GeometryReader背景,实时读取内容尺寸并更新状态变量
  2. 给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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.24 23:55:00