SwiftUI中VStack无法随ForEach内容正确调整尺寸求助
我构建了如下SwiftUI界面(用于复现问题的简化实现):
class IdentifiableString: Identifiable, Hashable { static func ==(lhs: IdentifiableString, rhs: IdentifiableString) -> Bool { lhs.id == rhs.id } func hash(into hasher: inout Hasher) { hasher.combine(self.id) } let id = UUID() var string = "" } struct ContentView: View { @State var textField1Text = "" @State var textField2Text = "" @State var textField3Text = "" @State var textField4Text = "" @State var textField5Text = "" @State var textField6Text = "" @State var textField7Text = "" @State var textField8Text = "" @State var textField9Text = "" @State var textField10Text = "" @State var textField11Text = "" @State var textField12Text = "" @State var textField13Text = "" @State var textField14Text = "" @State var textField15Text = "" @State var identifibleStrings = [IdentifiableString(), IdentifiableString(), IdentifiableString(), IdentifiableString(), IdentifiableString(), IdentifiableString(), IdentifiableString(), IdentifiableString(), IdentifiableString(), IdentifiableString(), IdentifiableString(), IdentifiableString(), IdentifiableString(), IdentifiableString(), IdentifiableString()] var body: some View { List { Button("Reset", role: .destructive) { textField1Text = "" textField2Text = "" textField3Text = "" textField4Text = "" textField5Text = "" textField6Text = "" textField7Text = "" textField8Text = "" textField9Text = "" textField10Text = "" textField11Text = "" textField12Text = "" textField13Text = "" textField14Text = "" textField15Text = "" for identifibleString in identifibleStrings { identifibleString.string = "" } } Section { TextField("TextField 1", text: $textField1Text) .onChange(of: textField1Text) { _, newValue in identifibleStrings[0].string = newValue } TextField("TextField 2", text: $textField2Text) .onChange(of: textField2Text) { _, newValue in identifibleStrings[1].string = newValue } TextField("TextField 3", text: $textField3Text) .onChange(of: textField3Text) { _, newValue in identifibleStrings[2].string = newValue } TextField("TextField 4", text: $textField4Text) .onChange(of: textField4Text) { _, newValue in identifibleStrings[3].string = newValue } TextField("TextField 5", text: $textField5Text) .onChange(of: textField5Text) { _, newValue in identifibleStrings[4].string = newValue } TextField("TextField 6", text: $textField6Text) .onChange(of: textField6Text) { _, newValue in identifibleStrings[5].string = newValue } TextField("TextField 7", text: $textField7Text) .onChange(of: textField7Text) { _, newValue in identifibleStrings[6].string = newValue } TextField("TextField 8", text: $textField8Text) .onChange(of: textField8Text) { _, newValue in identifibleStrings[7].string = newValue } TextField("TextField 9", text: $textField9Text) .onChange(of: textField9Text) { _, newValue in identifibleStrings[8].string = newValue } TextField("TextField 10", text: $textField10Text) .onChange(of: textField10Text) { _, newValue in identifibleStrings[9].string = newValue } TextField("TextField 11", text: $textField11Text) .onChange(of: textField11Text) { _, newValue in identifibleStrings[10].string = newValue } TextField("TextField 12", text: $textField12Text) .onChange(of: textField12Text) { _, newValue in identifibleStrings[11].string = newValue } TextField("TextField 13", text: $textField13Text) .onChange(of: textField13Text) { _, newValue in identifibleStrings[12].string = newValue } TextField("TextField 14", text: $textField14Text) .onChange(of: textField14Text) { _, newValue in identifibleStrings[13].string = newValue } TextField("TextField 15", text: $textField15Text) .onChange(of: textField15Text) { _, newValue in identifibleStrings[14].string = newValue } } Section { VStack { ForEach(identifibleStrings) { identifiableString in if identifiableString.string != "" { Text(identifiableString.string) } } } } } } }
问题现象
- 给所有TextField输入内容后点击Reset按钮,滚动到
VStack所在区域时,它仍保持原有大尺寸;只有将VStack滚出屏幕再返回,才会正确调整为空白的尺寸。 - 若点击Reset时
VStack在屏幕可见范围内,该问题不会出现(例如注释掉TextField10-15,或在iPad等大屏设备运行)。 - 偶尔会遇到
VStack无法充分展开以显示全部内容的情况,但未能稳定复现。
尝试过用@State变量控制VStack的显示、给Text添加.fixedSize(horizontal: true, vertical: false)修饰符,均无效果。
核心问题在于IdentifiableString是引用类型(类),SwiftUI的@State仅会在值类型的数组本身发生变化时(比如元素增删、替换)触发视图更新。当你修改类实例的string属性时,数组的内存地址并没有改变,@State无法检测到这种内部属性的变化,因此不会通知视图重新计算布局。
当VStack不在屏幕可视区域时,SwiftUI会对这部分视图进行懒加载或缓存,不会实时更新其尺寸。只有当视图重新进入可视区域时,SwiftUI才会重新计算布局,此时才会发现string属性已清空,从而调整VStack的尺寸。
方案1:将类改为结构体(推荐)
SwiftUI更适配值类型,把IdentifiableString改成结构体,这样修改其属性时,数组中的元素会被替换为新的实例,@State能检测到数组的变化,触发视图更新:
struct IdentifiableString: Identifiable, Hashable { let id = UUID() var string = "" }
同时修改Reset按钮和TextField的onChange逻辑(结构体是值类型,需要替换数组中的元素):
Button("Reset", role: .destructive) { // 清空所有TextField文本 textField1Text = "" textField2Text = "" textField3Text = "" textField4Text = "" textField5Text = "" textField6Text = "" textField7Text = "" textField8Text = "" textField9Text = "" textField10Text = "" textField11Text = "" textField12Text = "" textField13Text = "" textField14Text = "" textField15Text = "" // 替换数组中的元素为新的空实例 identifibleStrings = identifibleStrings.map { item in var newItem = item newItem.string = "" return newItem } } // TextField的onChange修改为: TextField("TextField 1", text: $textField1Text) .onChange(of: textField1Text) { _, newValue in identifibleStrings[0] = IdentifiableString(id: identifibleStrings[0].id, string: newValue) }
方案2:使用ObservableObject包装类实例
如果必须使用类,可以让IdentifiableString遵循ObservableObject,并用@Published标记string属性,同时在数组中监听每个实例的变化:
class IdentifiableString: Identifiable, Hashable, ObservableObject { static func ==(lhs: IdentifiableString, rhs: IdentifiableString) -> Bool { lhs.id == rhs.id } func hash(into hasher: inout Hasher) { hasher.combine(self.id) } let id = UUID() @Published var string = "" }
然后修改ForEach部分,添加辅助结构体监听每个实例的变化:
Section { VStack { ForEach(identifibleStrings) { identifiableString in ObservedObjectWrapper(object: identifiableString) { obj in if obj.string != "" { Text(obj.string) } } } } } // 辅助结构体:用于在ForEach中监听ObservableObject实例 struct ObservedObjectWrapper<T: ObservableObject, Content: View>: View { @ObservedObject var object: T let content: (T) -> Content var body: some View { content(object) } }
方案3:手动触发视图刷新
如果不想修改数据结构,可以通过修改一个无关的@State变量来强制视图刷新:
struct ContentView: View { // ... 原有State变量 @State private var refreshTrigger = false var body: some View { List { Button("Reset", role: .destructive) { // ... 原有清空逻辑 textField1Text = "" textField2Text = "" // ... 其他TextField for identifibleString in identifibleStrings { identifibleString.string = "" } // 手动触发刷新 refreshTrigger.toggle() } // ... 其他视图 Section { VStack { ForEach(identifibleStrings) { identifiableString in if identifiableString.string != "" { Text(identifiableString.string) } } } .id(refreshTrigger) // 通过id变化强制视图重建 } } } }
内容的提问来源于stack exchange,提问作者Silver-Elder

