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

SwiftUI中ScrollView高度不更新致RadioButtonGroup无法显示

SwiftUI ScrollView高度未更新导致RadioButtonGroup显示异常问题

我在SwiftUI开发中实现了一个由自定义按钮组成的RadioButtonGroup视图,视图的数据通过ViewModel的@Published字段获取,并以@Binding形式传递给视图。目前遇到的问题是ScrollView的高度未更新,导致RadioButtonGroup无法适配显示。

相关代码

主视图代码

import Foundation
import SwiftUI
import PhotosUI

struct SignupView: View {
    
    @StateObject var viewModel: SignUpVM
    
    @State private var selectedPos = ""
    
    var body: some View {
        
        ScrollView(showsIndicators: false){
            VStack( spacing: 24){
                
                Text("Select your position")
                    .font(UIConstraints.fontRegular(size: 18))
                    .frame(maxWidth: .infinity, alignment: .leading)
                
                RadioButtonGroup<Position>(items: $viewModel.positions, selectedId: "London") { selected in
                    
                }
                
                Spacer()
            }
            .padding(.vertical, 32)
            .padding(.horizontal, 16)
        }
    }
}

struct ContentView_Previews4: PreviewProvider {
    struct Wrapper: View {
        @State private var isPresented: UIImage? = UIImage(named: "photo-cover")
        @StateObject var vm = SignUpVM()
        
        var body: some View {
            SignupView(viewModel: vm)
        }
    }
    
    static var previews: some View {
        Wrapper()
    }
}

RadioButton相关代码

import Foundation
import SwiftUI

protocol RadioButtonItem {
    var id: Int { get set }
    var name: String { get set }
}

struct RadioButtonGroup<T: RadioButtonItem>: View {
    
    @Binding var items : [T]
    
    @State var selectedId: String = ""
    
    let callback: (String) -> ()
    
    var body: some View {
        VStack {
            List(items, id: \.id) { item in
                RadioButton(item.name, callback: self.radioGroupCallback, selectedID: self.selectedId)
                    .listRowSeparator(.hidden)
                    .listRowInsets(EdgeInsets())
            }
            .listStyle(PlainListStyle())
        }
    }
    
    func radioGroupCallback(id: String) {
        selectedId = id
        callback(id)
    }
}

struct RadioButton: View {
    
    @Environment(\.colorScheme) var colorScheme
    
    let id: String
    let callback: (String)->()
    let selectedID : String
    let size: CGFloat
    let color: Color
    let textSize: CGFloat
    
    init(
        _ id: String,
        callback: @escaping (String)->(),
        selectedID: String,
        size: CGFloat = 20,
        color: Color = Color.primary,
        textSize: CGFloat = 14
    ) {
        self.id = id
        self.size = size
        self.color = color
        self.textSize = textSize
        self.selectedID = selectedID
        self.callback = callback
    }
    
    var body: some View {
        Button(action:{
            self.callback(self.id)
        }) {
            HStack(alignment: .center, spacing: 10) {
                Image(self.selectedID == self.id ? "radio-button-selected" : "radio-button-unselected")
                    .renderingMode(.original)
                    .resizable()
                    .frame(width: 17, height: 17)
                    .aspectRatio(contentMode: .fit)
                    .foregroundColor(self.selectedID == self.id ? UIConstraints.secondary : .gray)
                    .padding(17)
                
                Text(id)
                    .font(UIConstraints.fontRegular(size: 16))
                Spacer()
            }.foregroundColor(self.color)
        }
        .foregroundColor(self.color)
        .padding(0)
    }
}

struct InputTextField_PreviewsRadio: PreviewProvider {
    struct Wrapper: View {
        
        @StateObject var viewModel = SignUpVM()
        
        var body: some View {
            RadioButtonGroup<Position>(items: $viewModel.positions, selectedId: "London") { selected in
                
            }
        }
    }

    static var previews: some View {
        Wrapper()
    }
}

问题原因及解决方案

核心原因

  1. List与ScrollView的布局冲突:RadioButtonGroup内部使用List作为容器,而List本身自带滚动特性,嵌套在ScrollView中时会导致布局计算异常,ScrollView无法正确识别内容高度。
  2. 多余的Spacer占用空间:主视图VStack中的Spacer()会强制撑开剩余空间,干扰ScrollView对内容高度的计算。

修复步骤

1. 替换List为VStack

将RadioButtonGroup中的List替换为ForEach+VStack,避免嵌套滚动组件的布局冲突:

struct RadioButtonGroup<T: RadioButtonItem>: View {
    
    @Binding var items : [T]
    @State var selectedId: String = ""
    let callback: (String) -> ()
    
    var body: some View {
        VStack(spacing: 0) { // 用VStack替代List,控制元素间距
            ForEach(items, id: \.id) { item in
                RadioButton(item.name, callback: self.radioGroupCallback, selectedID: self.selectedId)
            }
        }
    }
    
    func radioGroupCallback(id: String) {
        selectedId = id
        callback(id)
    }
}

2. 移除主视图中的Spacer

删除SignupView中VStack内的Spacer(),让ScrollView根据实际内容计算高度:

struct SignupView: View {
    
    @StateObject var viewModel: SignUpVM
    @State private var selectedPos = ""
    
    var body: some View {
        ScrollView(showsIndicators: false){
            VStack( spacing: 24){
                Text("Select your position")
                    .font(UIConstraints.fontRegular(size: 18))
                    .frame(maxWidth: .infinity, alignment: .leading)
                
                RadioButtonGroup<Position>(items: $viewModel.positions, selectedId: "London") { selected in
                    selectedPos = selected // 可选:处理选中回调
                }
                // 移除Spacer()
            }
            .padding(.vertical, 32)
            .padding(.horizontal, 16)
        }
    }
}

3. 可选:添加分隔线模拟List样式

如果需要保留List的视觉分隔效果,可以在RadioButton底部添加Divider:

// 修改RadioButton的body
var body: some View {
    Button(action:{
        self.callback(self.id)
    }) {
        HStack(alignment: .center, spacing: 10) {
            // 原有代码...
        }
    }
    .foregroundColor(self.color)
    .padding(0)
    .overlay(alignment: .bottom) {
        Divider() // 添加底部分隔线
            .padding(.horizontal, 16)
    }
}

验证要点

  • 确认ViewModel的positions字段已标记为@Published,确保数据更新时视图能自动刷新
  • 确保Position类型正确遵循RadioButtonItem协议,且id字段唯一,避免ForEach刷新异常

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.19 00:00:00