I have a view that displays core data values (passed in as closingValues) in a data table. To make the view generic, so I can pass in arrays of different lengths (all are from the same entity and thus attributes) and account for the view having already been called with a different set of values, I need to initialize the showData array contained in the associated view model. I attempt to do so in the init of the view but I get an error message "publishing changes from within a view updates is not allowed. This will cause undefined behavior". It should be noted that in the view model showData is Published and initialized = []. I attempted to do the initialization in the views onappear but the tables rows: ForEach runs before onappear and I get an index out of range on the first call to the view. I tried adding @MainActor to the view models class, which also failed to solve the problem. I am clearly not understanding a concept. Below is the code for the view.
struct DataTable: View {
@ObservedObject var vm: ButtonsViewModel = ButtonsViewModel.shared
var closingValues: [TradingDayClose]
var heading: String = ""
init(fundName: String, closingValues: [TradingDayClose]) {
self.heading = fundName
self.closingValues = closingValues
vm.showData = []
vm.showData = Array(repeating: false, count: closingValues.count)
// Publishing changes from within view updates is not allowed, this will cause undefined behavior.
}
var body: some View {
HStack {
Spacer()
.frame(width: 150)
GroupBox(heading) {
Table (of: TradingDayClose.self) {
TableColumn("") { closingValue in
Text(dateToStringFormatter.string(from: closingValue.timeStamp!))
.id(closingValue.timeStamp!)
.textFormatting(fontSize: 14)
.frame(width: 100, alignment: .center)
} // end table column
TableColumn("") { closingValue in
Text(String(format: "$ %.2f", closingValue.close))
.textFormatting(fontSize: 14)
.frame(width: 100, alignment: .center)
} // end table column
} rows: {
ForEach((closingValues.indices), id: \.self) { index in
if vm.showData[index] == true {
TableRow(closingValues[index])
}
}
}
.onAppear {
vm.InitializeIndexes()
vm.InitializeButtons()
for i in vm.startIndex...vm.endIndex {
DispatchQueue.main.asyncAfter(deadline: .now() + vm.renderRate * Double(i)) {
vm.showData[i] = true
} // end dispatch queue main async
}
}
.frame(width: 250)
.overlay {
let tempValue1: String = "Date"
let tempValue2: String = "Closing Value"
Text(tempValue1).position(x: 63, y: 15)
.textFormatting(fontSize: 16)
Text(tempValue2).position(x: 180, y: 15)
.textFormatting(fontSize: 16)
}
} // end group box
.groupBoxStyle(Table2GroupBoxStyle())
Spacer()
.frame(width: 50)
VStack {
Spacer()
.frame(height: 100)
ButtonUp25(closingValuesCount: closingValues.count)
ButtonUp200(closingValuesCount: closingValues.count)
Spacer()
.frame(height: 20)
ButtonDown25(closingValuesCount: closingValues.count)
ButtonDown200(closingValuesCount: closingValues.count)
Spacer()
.frame(height: 100)
} // end v stack
Spacer()
.frame(width: 50)
} // end h stack
} // end body
} // end struct
Doing in onAppear is a good option.
But apply onAppear to the global HStack that includes the Table, not the Table itself.
If that does not work, you could:
- create a State var
@State private var hasAppeared = false
- set it true in .onAppear
- condition the display of Table with if hasAppeared