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() } }
问题原因及解决方案
核心原因
- List与ScrollView的布局冲突:RadioButtonGroup内部使用
List作为容器,而List本身自带滚动特性,嵌套在ScrollView中时会导致布局计算异常,ScrollView无法正确识别内容高度。 - 多余的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
相关产品推荐
相关产品推荐

