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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.29 10:34:55