Search code examples
swiftuiswiftui-navigationlink

Strange issue of "Modifying state during view update, this will cause undefined behavior"


Goal: have a SwiftUI architecture where the "add new item" and "edit existing item" are solved by the same view (EditItemView). However, for some reason, when I do this, the runtime agent complains of "Modifying state during view update, this will cause undefined behavior".

This is the code I want to use, which ensures that the EDITING of the item and ADDING a new item are handled by the same EditItemView:

var body: some View {
    NavigationView
    {
        
        ScrollView
        {
            LazyVGrid(columns: my_columns)
            {
                ForEach(items, id: \.id)
                {
                    let item = $0
                    
                    // THIS LINE TO EDIT AN EXISTING ITEM
                    NavigationLink(destination: EditItemView(item: item))
                    {
                        ItemView(item: item)
                    }
                    
                }

            }
            
        }
        .navigationBarItems(trailing:
            // THIS LINE TO ADD A NEW ITEM:
            NavigationLink(destination: EditItemView(item: Data.singleton.createItem(name: "New item", value: 5.0))
            {
                Image(systemName: "plus")
            }
        )
    }
}

It doesn't work, leading to the issue highlighted above. I am forced to separate the functionality for Edit and Add into two distinct Views, which then works:

var body: some View {
    NavigationView
    {
        
        ScrollView
        {
            LazyVGrid(columns: my_columns)
            {
                ForEach(items, id: \.id)
                {
                    let item = $0
                    
                    // THIS LINE TO EDIT AN EXISTING ITEM
                    NavigationLink(destination: EditItemView(item: item))
                    {
                        ItemView(item: item)
                    }
                    
                }

            }
            
        }
        .sheet(isPresented: $isPresented)
        {
            // FORCED TO USE SEPARATE VIEW
            AddItemView { name, value in
                _ = Data.singleton.createItem(name: name, value: value)
                self.isPresented = false
            }
        }
        .navigationBarItems(trailing: Button(action: { self.isPresented.toggle()}) { Image(systemName: "plus")})
    }
}

I don't understand why the code in the first version is considered to modify the state while updating view, because to me, it's sequential: new Item is created and THEN a view is shown for that Item.

Any ideas?


Solution

  • The destination of NavigationLink is not rendered lazily, meaning it'll get rendered when the NavigationLink itself is rendered -- not when clicked through.

    The sheet code, depending on platform and SwiftUI version, may have the same issue, but apparently does not in the version you're using. Or, the closure you provide to AddItemView isn't run immediately -- since you didn't include the code, it's not clear.

    To solve the issue in the first method, you can use the following SO answer which provides a lazy NavigationLink: https://stackoverflow.com/a/61234030/560942