Consider the following setup (it's not the neatest display, but it was the quickest way I could think of to replicate my problem):
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)
}
}
}
}
}
}
}
I've discovered that when I provide all of my text fields with some kind of input, tap on the reset button, and then scroll down to my VStack
, I see this:
and only when I scroll my VStack off screen and back again do I find that it has properly resized itself:
Note: This behavior doesn't occur if the VStack's row is visible on the UI when the reset button is tapped (e.g. comment out text fields 10-15, or run the code on a device with a substantially larger screen (such as an iPad)).
I've also occasionally had the VStack not grow sufficiently to display all of its contents properly, but I haven't been able to consistently replicate that behavior. I attempt to do this by creating a "Set with Dummy Data" button that automatically populated all my text fields with data, but the VStack grew to the expected size.
I tried wrapping my VStack
in an if
block that would only allow the VStack
to show if an @State
bool variable in my view struct was set to false
:
@State var resetUpdate = false
Button("Set with Dummy Data") {
resetUpdate = true
...
}
.onChange(of: resetUpdate) { _, newValue in
if newValue == true {
resetUpdate = false
}
}
if !resetUpdate {
VStack {
...
But that had no effect. Using .fixedSize(horizontal: true, vertical: false)
on the Text
view in the ForEach
loop doesn't do anything either. Does anyone have any guesses as to why this behavior is occurring (as well as how to prevent it)?
Note @State var ...
works with value type, eg struct
, not reference type, eg class
, unless you use the Observation framework.
Try this approach using @Observable class IdentifiableString
and some ForEach
loops to simplify the code.
It seems that the View does not rerender the VStack
properly (probably another SwiftUI bug).
To force it to do so, I used an .id()
and change its value
when the reset button is tapped.
Example code that works for me:
@Observable class IdentifiableString: Identifiable {
let id = UUID()
var string = "xxxx" // <--- for testing
init() { }
}
struct ContentView: View {
@State private var items = [IdentifiableString]()
@State private var id = false // <--- here
var body: some View {
List {
Button("Reset", role: .destructive) {
items.forEach{ $0.string = ""}
id.toggle() // <--- here
}
Section {
ForEach($items) { $item in
TextField("TextField", text: $item.string)
}
}
Section {
VStack {
ForEach(items) { item in
if !item.string.isEmpty {
Text(item.string)
}
}
}
.id(id) // <--- here, or on Section, or List
}
}
.onAppear {
for _ in 0...24 { items.append(IdentifiableString())}
}
}
}
Edit-1:
If you really need to have a ObservableObject
then use this code,
do not nest ObservableObject
and do not use your static func == ..
based on id
, and no need for func hash...
.
Also use @StateObject
not @ObservedObject
, such as:
struct IdentifiableString: Identifiable { // <--- here
let id = UUID()
var string = "xxxx" // <--- for testing
}
class ItemsModel: ObservableObject {
@Published var items = [IdentifiableString]()
}
struct ContentView: View {
@StateObject private var dataModel = ItemsModel() // <--- here
@State private var id = false // <--- here
var body: some View {
List {
Button("Reset", role: .destructive) {
dataModel.items.indices.forEach{ dataModel.items[$0].string = ""}
id.toggle() // <--- here
}
Section {
ForEach($dataModel.items) { $item in
TextField("TextField", text: $item.string)
}
}
Section {
VStack {
ForEach(dataModel.items) { item in
if !item.string.isEmpty {
Text(item.string)
}
}
}
.id(id) // <--- here, or on Section, or List
}
}
.onAppear {
for _ in 0...24 { dataModel.items.append(IdentifiableString())}
}
}
}