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.

1

There are 1 best solutions below

0
AndresV On

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

import SwiftUI
import SwiftData


struct AddItemView: View {
    
    //SwiftData
    @Query var categories: [CategoryDataModel]
    @Environment(\.modelContext) var modelContext
    @Binding var selection: Int
    @State var item = ItemDataModel(name: "", location: "", category: "Miscellaneous", notes: "")
    
    
    var body: some View {
        VStack {
            NavigationView {
                
                FormView(nextScreen: changeScreen, item: $item )
                
                .navigationTitle("Add Item")
                
            } //end navigationView
        }// end vstack
    }// end body
    
    func changeScreen(){
        selection = 2
    }
}



#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 AddItemView(selection: .constant(2))
        .modelContainer(container)
    
}

FormView


import SwiftUI
import SwiftData
import PhotosUI

struct FormView: View {
    var nextScreen: () -> Void
    
    @Query var categories: [CategoryDataModel]
    @Environment(\.modelContext) var modelContext
    
    //for the photo picker feature
    @State private var photoPickerItem: PhotosPickerItem?
    @State var avatarImage: UIImage?
    
    //these will be saved using Swift Data using ItemDataModel
    @State private var name = ""
    @State private var category = ""
    @State private var location = ""
    @State private var notes = ""
    @State private var imageData: Data?
    
    @Binding var item: ItemDataModel
    
    
    var body: some View {
        
        
        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
                }                  
                //-------------END PHOTO PICKER SECTION-------------------------------
                
                // MARK: 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 emptyItem = ItemDataModel(name: "", location: "", category: "Miscellaneous", notes: "")
                    item.name = name
                    item.location = location
                    item.category = category
                    item.image = imageData
                    modelContext.insert(item)
                    
                    
                    //CLEAR FORM WHEN FINISHED
                    name = ""
                    category = ""
                    location = ""
                    self.notes = ""
                    imageData = nil
                    avatarImage = nil
                    item = emptyItem     //RESETTING item
                    
                    nextScreen()
                    
                }
                //input validation to ensure name and location are filled out
                .disabled(name.isEmpty || location.isEmpty)
                Spacer()
            }//end hstack
            //putting the screen change button here does NOT work
            
        }//end form
    }
}

#Preview {
    let container = try! ModelContainer(for: CategoryDataModel.self, ItemDataModel.self, configurations: ModelConfiguration(isStoredInMemoryOnly: true))
    let tempArray = ["Miscellaneous"]
    let newCategory = CategoryDataModel(categoryList: tempArray)
    let tempItem = ItemDataModel(name: "", location: "", category: "Miscellaneous", notes: "")
    container.mainContext.insert(newCategory)
    func nextScreenPreview() {
        
    }
    return FormView(nextScreen: nextScreenPreview, item: .constant(tempItem))
        .modelContainer(container)
}