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

SwiftUI:如何让ScrollView底部HStack按钮固定位置并保持间距

解决方案

要实现底部按钮始终固定在页面底部,同时保持与最后一个RatingView的40间距、与安全区底部的8间距,你需要重构视图布局,让底部按钮脱离滚动内容流,具体改动如下:

核心改动点

  • 将底部按钮HStack从ScrollView内部移出,放在GeometryReader层级的VStack中与ScrollView并列,让按钮固定在屏幕底部,不受内容滚动影响。
  • 给底部按钮添加.padding(.bottom, 8),保证距离安全区底部的间距。
  • 在ScrollView的内容末尾添加.padding(.bottom, 40),确保最后一个RatingView和底部按钮之间始终保持40的间距。
  • 调整未展开状态下ScrollView的高度计算,避免内容被底部按钮遮挡。

修改后的完整代码

import SwiftUI

struct SH1ViewResult: View {
    @State var text: String = "long text long text long text long textlong text long textlong text long textlong text long textlong text long textlong text long textlong text long textlong text long textlong text long textlong text long textlong text long textlong text long textlong text long textlong text long textlong text long textlong text long textlong text long textlong text long textlong text long textlong text long textlong text long textlong text long textlong text long textlong text long textlong text long textlong text long textlong text long textlong text long textlong text long textlong text long textlong text long textlong text long textlong text long textlong text long textlong text long textlong text long textlong text long textlong text long textlong text long textlong text long textlong text long textlong text long textlong text long textlong text long textlong text long textlong text long textlong text long textlong text long textlong text long textlong text long textlong text long textlong text long textlong text long textlong text long textlong text long textlong text long textlong text long textlong text long text"
    @State var userVoice: Bool = false
    @State var isExpanded: Bool = false
    @State var score: Int = 69
    var quit: () -> Void

    var body: some View {
        GeometryReader { geometry in
            VStack(spacing: 0) { // 外层VStack包裹滚动内容和底部按钮
                ScrollView {
                    VStack(spacing: 0) {
                        Rectangle()
                            .background(Color.blue)
                            .frame(width: .infinity, height: 50)
                        
                        VStack {
                            ScrollView {
                                Text("\(text)")
                                    .padding([.top, .bottom, .leading, .trailing], 12)
                            }
                            .disabled(isExpanded)
                        }
                        .background(Color.green)
                        .frame(width: geometry.size.width - 32,
                               height: isExpanded ? .infinity : geometry.size.height * 0.40)
                        .overlay(
                            VStack {
                                Spacer()
                                Image("Chevron")
                                    .renderingMode(.template)
                                    .resizable()
                                    .frame(width: 8, height: 13)
                                    .foregroundColor(Color.black)
                                    .rotationEffect(.degrees(-90))
                                    .padding(.bottom, 4)
                                    .onTapGesture {
                                        isExpanded = true
                                    }
                            }
                                .frame(width: geometry.size.width - 32, height: 40)
                                .background(
                                    LinearGradient(gradient:
                                                    Gradient(colors:
                                                                [Color.green,
                                                                 Color.gray]),
                                                   startPoint: .top,
                                                   endPoint: .bottom
                                                  )
                                )
                                .opacity(isExpanded ? 0 : 1),
                            alignment: .bottom
                        )
                        .cornerRadius(10)
                        
                        HStack(spacing: 24) {
                            Button {
                                print("example sound")
                            } label: {
                                Image(systemName: "")
                                    .resizable()
                                    .frame(width: 108, height: 42)
                            }
                            Button {
                                print("uservoice")
                            } label: {
                                Image(systemName: "")
                                    .resizable()
                                    .frame(width: 108, height: 42)
                            }
                        }
                        .padding(.top, 16)
                        
                        VStack {
                            RatingStarsView(score: .constant(55),
                                            labelIsShow: .constant(true)
                            )
                        }
                        .padding(.top, 16)
                        
                        VStack {
                            HStack {
                                Spacer()
                                Text("first")
                                    .frame(maxWidth: .infinity)
                                RatingStarsView(score: .constant(72),
                                                labelIsShow: .constant(false)
                                )
                                .frame(maxWidth: .infinity)
                                Spacer()
                            }
                            .padding(.bottom, 24)
                            .frame(width: geometry.size.width - 64, height: 30)
                            
                            HStack {
                                Spacer()
                                Text("second")
                                    .frame(maxWidth: .infinity)
                                RatingStarsView(score: .constant(85),
                                                labelIsShow: .constant(false)
                                )
                                .frame(maxWidth: .infinity)
                                Spacer()
                            }
                            .frame(width: geometry.size.width - 64, height: 30)
                        }
                        .padding(.top, 22)
                        
                        // 固定与底部按钮的间距
                        .padding(.bottom, 40)
                    }
                    .frame(height: isExpanded ? .infinity : geometry.size.height - 80) // 未展开时预留按钮空间
                }
                .scrollEnabled(isExpanded)
                
                // 底部按钮区域,固定在屏幕底部
                HStack(spacing: 20) {
                    Button {
                        print("button")
                    }  label: {
                        Text("button 1")
                            .font(.system(size: 17, weight: .medium))
                            .frame(maxWidth: .infinity, maxHeight: 10)
                            .padding([.leading, .trailing], 16 )
                    }
                    .buttonStyle(LightGreenButton())
                    .disabled(userVoice ? true : false)
                    
                    Button {
                        print("tapped next")
                    }  label: {
                        Text("button 2")
                            .font(.system(size: 17, weight: .medium))
                            .frame(maxWidth: .infinity, maxHeight: 10)
                            .padding([.leading, .trailing], 16)
                    }
                    .buttonStyle(FilledButton())
                    .disabled(userVoice ? true : false)
                }
                .padding([.leading, .trailing], 16)
                .padding(.bottom, 8) // 距离安全区底部8个单位
                .frame(width: geometry.size.width - 32)
            }
        }
    }
}

extension View {
    @ViewBuilder func scrollEnabled(_ enabled: Bool) -> some View {
        if enabled {
            self
        } else {
            simultaneousGesture(DragGesture(minimumDistance: 0),
                                including: .all)
        }
    }
}

// 补充原代码缺失的ButtonStyle实现
struct LightGreenButton: ButtonStyle {
    func makeBody(configuration: Configuration) -> some View {
        configuration.label
            .padding()
            .background(Color.green.opacity(0.5))
            .cornerRadius(8)
    }
}

struct FilledButton: ButtonStyle {
    func makeBody(configuration: Configuration) -> some View {
        configuration.label
            .padding()
            .background(Color.green)
            .foregroundColor(.white)
            .cornerRadius(8)
    }
}

// 补充原代码缺失的RatingStarsView实现
struct RatingStarsView: View {
    @Binding var score: Int
    @Binding var labelIsShow: Bool
    var body: some View {
        HStack {
            if labelIsShow {
                Text("\(score)分")
            }
            ForEach(0..<5) { index in
                Image(systemName: index < score/20 ? "star.fill" : "star")
                    .foregroundColor(.yellow)
            }
        }
    }
}

说明

  • 外层VStack将滚动内容和底部按钮分离,按钮始终处于屏幕底部,不会随内容滚动。
  • .padding(.bottom, 40) 确保滚动内容的最后一个元素与按钮的间距固定,无论内容是否展开都保持一致。
  • 未展开状态下ScrollView的高度计算geometry.size.height - 80是估算值,你可以根据按钮实际高度和布局需求调整这个数值,避免内容被底部按钮遮挡。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.11 18:15:54