“在视图更新期间修改状态,这将导致未定义的行为”使用表单内的 tabview 切换屏幕时控制台警告

问题描述 投票:0回答:1

这是我使用 SwiftUI 的第一个项目,就像家庭库存跟踪器一样,我遇到了一个我不知道如何解决的问题。我的代码运行且功能正常,但当按下按钮提交数据并将其从表单保存到 SwiftData 时,控制台会输出相同警告的 2 个副本:“在视图更新期间修改状态,这将导致未定义的行为。” 我设法将问题根源缩小到 form{} 中的 TextFields。当我注释掉表单闭包中的 TextFields 时,控制台中的警告消息消失了。我似乎找不到任何东西来找到如何处理这个问题。

本质上,我有一个 AddItemsView 文件,它是添加项目的屏幕。它包含要添加的字段:

  • 名称(字符串)
  • 位置(字符串)
  • 图像(使用 PhotoPicker() )
  • 类别(使用 Picker() 的字符串)
  • 注释(字符串)

我有另一个名为 TabBarView 的文件,其中包含 TabView,可使用 Int 设置屏幕来在 3 个选项卡/屏幕之间切换:

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)

    
}

这是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

我尝试使用一个函数来更改 tabview 值来更改屏幕并将其定义在正文之外,但这不起作用。我尝试将 self 添加到

selection = 2
成为
self.selection = 2
我知道我必须更改视图之外的屏幕,以免在视图更新时干扰,但我不知道如何。

注释掉 SwiftData 组件时仍然出现该错误,因此我认为发布我的 SwiftData 模型不会有所帮助,但如果有帮助,我会很乐意展示它。这真的很难过,到目前为止我从未在网上发布过问题。我正在考虑不要管它,因为应用程序运行良好,但我读到有人的声明说苹果可以以任何方式更新 SwiftUI,这个警告实际上可能会在稍后回来咬我,所以我决定现在修复它。

我看到一篇文章说我必须在视图之外进行更改,这样就不会干扰 SwiftUI 在后台重建视图,他们使用 onAppear 来包含触发错误的示例代码,但我不知道如何让它与 TextField 一起使用。

我真的很喜欢 TabView 在我的应用程序中的外观,但我愿意接受其他方法来处理它以避免此错误。

swift forms swiftui textview
1个回答
0
投票

我找到了解决办法。我将表单放置在其自己的名为 FormView 的视图文件中,并为 ItemDataModel 数据模型实例初始化了一个状态变量。 我将其传递到 FormView 中,与 AddItemView 中定义的 changeScreen() 函数一起用作绑定。

FormView 看起来与 AddItemView 中的表单完全相同,只是保存按钮将用户输入保存到绑定的 ItemDataModel 变量中,然后调用changeScreen(),它只是更改 TabView 选择值。

由于 changeScreen() 未在 FormView() 中定义,而是在父视图中定义,因此它不会干扰 TextFields 和其他表单输入,从而避免“视图更新期间修改状态”错误。

修改了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) }
表单视图

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) }
    
© www.soinside.com 2019 - 2024. All rights reserved.