This is my first project with SwiftUI that is like a home inventory tracker and i have come to a problem I do not know how to solve. My code runs and functions fine but when pressing a button to submit and save data from a form to SwiftData, the console spits out 2 copies of the same warning, "Modifying state during view update, this will cause undefined behavior." I managed to narrow down the source of the issue to the TextFields in form{}. When I commented out the TextFields in the form closure, the warning messages in the console disappeared. I can't seem to find anything to find how to handle this.
Essentially, I have an AddItemsView file that is the screen to add an item. It contains fields to add:
- name (String)
- location (String)
- image (using PhotoPicker() )
- category (String using Picker() )
- notes (String)
I have another file called TabBarView that contains TabView to switch between 3 tabs/screens using a Int to set the screen:
import SwiftUI
import SwiftData
struct TabBarView: View {
@State var selection = 2 //this is used with binding to switch screens from other files
var body: some View {
TabView(selection:$selection){
AddItemView(selection: $selection)
.tabItem{
Label("Add", systemImage: "plus")
}
.tag(1)
BrowseView()
.tabItem {
Label("Browse", systemImage: "list.dash")
}
.tag(2)
SearchView()
.tabItem {
Label("Search", systemImage: "magnifyingglass")
}
.tag(3)
}
}
}
#Preview {
let container = try! ModelContainer(for: CategoryDataModel.self, ItemDataModel.self, configurations: ModelConfiguration(isStoredInMemoryOnly: true))
let tempArray = ["Miscellaneous"]
let newCategory = CategoryDataModel(categoryList: tempArray)
container.mainContext.insert(newCategory)
return TabBarView()
.modelContainer(container)
}
This is AddItemView:
import SwiftUI
import SwiftData
import PhotosUI
struct AddItemView: View {
//SwiftData
@Query var items: [ItemDataModel]
@Query var categories: [CategoryDataModel]
@Environment(\.modelContext) var modelContext
@Binding var selection: Int //this is used to change the screen
//for the photo picker feature
@State private var photoPickerItem: PhotosPickerItem?
@State var avatarImage: UIImage?
//these will be saved using SwiftData using ItemDataModel
@State private var name = ""
@State private var category = ""
@State private var location = ""
@State private var notes = ""
@State private var imageData: Data?
var body: some View {
NavigationView{
Form{
//This section is for the required data fields
Section(header: Text("Required")){
TextField("Name", text: $name)
TextField("Location", text: $location)
}
//this section is for the optional data fields
Section(header: Text("Optional")){
//--------this lets users choose an image they own---------------------
// MARK: Photo Picker Section
PhotosPicker(selection: $photoPickerItem, matching: .images){
let chosenImage: UIImage? = avatarImage
if chosenImage != nil{
Image(uiImage: avatarImage!)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(maxWidth: 80)
} else{
Text("Choose Image")
}
}
.onChange(of: photoPickerItem){ _, _ in
Task{
if let photoPickerItem,
let data = try? await photoPickerItem.loadTransferable(type: Data.self){
if let image = UIImage(data: data){
avatarImage = image
imageData = data
}
}
photoPickerItem = nil
}
}
.frame(maxWidth: .infinity)
.alignmentGuide(.listRowSeparatorLeading) { viewDimensions in
return 0
} //this is cleared
//-------------END PHOTO PICKER SECTION-------------------------------
// Category Picker
Picker("Choose Category", selection: $category){
ForEach(categories[0].categoryList, id: \.self) { cat in
Text(cat)
}
}
TextField("Notes", text: $notes, axis: .vertical)
.padding()
}
//save button
HStack{
Spacer()
Button ("Save Item"){
let item = ItemDataModel(name: name, location: location, category: category, notes: notes)
item.image = imageData
//modelContext.insert(item) //commented out for debugging
//clears form after saving
name = ""
category = ""
location = ""
self.notes = ""
imageData = nil
avatarImage = nil
//sends user to BrowseView
selection = 2 //ever since this line was added, the modifying state error has appeared. UPDATE: SOURCE FOUND - it was the textfields in form closure. commenting them out revealed the source of the error.
}
//input validation to ensure name and location are filled out
.disabled(name.isEmpty || location.isEmpty)
Spacer()
}
}
.navigationTitle("Add Item")
}
}// end body
}
#Preview {
let container = try! ModelContainer(for: CategoryDataModel.self, configurations: ModelConfiguration(isStoredInMemoryOnly: true))
let tempArray = ["Miscellaneous"]
let newCategory = CategoryDataModel(categoryList: tempArray)
container.mainContext.insert(newCategory)
return AddItemView(selection: .constant(2))
.modelContainer(container)
}
Image of the AddItemView screen Image of the BrowseView screen which the AddItemView screen displays after submitting item
I tried using a function to change the tabview value to change the screen and defined it outside the body but that did not work. I tried adding self to selection = 2 to become self.selection = 2 I know i have to change the screen outside of the view so as to not interfere while the view updates but I do not know how.
The error still appears when commenting out SwiftData components so I don't think posting my SwiftData model would help but if it would I'll gladly show it. This has genuinely stumped, I never have posted a question online before up to this point. I was considering just leaving it alone since the app runs fine but I read someone's statement saying that Apple could update SwiftUI in any way that this warning could actually come back to bite me later so I decided to fix it now.
I saw a post saying that i have to make changes outside the view so that it does not interfere with SwiftUI rebuilding the view in the background and they used onAppear to contain their example code that triggered the error but I don't know how to get that to work with TextField.
I really like the way the TabView looks in my app but I am open to other ways to handle it just to avoid this error.
I found a solution. I placed the form in its own view file called FormView and initialized a state variable for an ItemDataModel data model instance. I passed that into FormView to use as binding along with the changeScreen() function that was defined in AddItemView.
FormView looks exactly the same as the form in AddItemView except that the save button saves the user inputs into the binded ItemDataModel variable and then calls changeScreen() which simply changes the TabView selection value.
Since changeScreen() is not defined in FormView() and instead is defined in the parent view, it does not interfere with the TextFields and other form inputs thus avoiding the 'Modifying state during view update' error.
Revised AddItemView
FormView