SwiftUI view is not updated properly

Task: A child view should show depending on a boolean flag the corresponding view. The flag is calculated from a model which is given by parent view.

Problem: The flag is false in init, which is correct. However in body it's true, which is incorrect. Why value in body isn't consistent to init? Is there a race condition? The child view's is rendered thrice, that's another issue, but flag is in init and body as described before.

Parent view looks like this:

struct ParentView: View {
   private var groupedItems: [GroupedItems] = []

   init(items: [Item]) {
      Dictionary(grouping: items) { $0.category.name }
          .forEach {
             let groupedItems = GroupedItems(categoryName: $0.key, items: $0.value)
             self.groupedItems.append(groupedItems)
          }
   }

   var body: some View {
       ChildView(groupedItems)
   }
}

Child view looks like this:

struct ChildView: View {
    @State private var showItems: Bool

     init(_ groupedItems: GroupedItems) {
         self._showItems = State(initialValue: !groupedItems.completed)
     }

     var body: some View {
         if showItems {
            AView()
         } else {
            BView()
         }
     }
}

Model looks like this:

@Model
final class Item: Identifiable {
    @Attribute(.unique) public var id: String
    public var name: String
    public var completed = false
}

struct GroupedItems {
    var categoryName: String
    var items: [Item]
    var completed: Bool {
        items.filter { !$0.completed }.isEmpty
    }
}
Answered by Chad S. in 794678022

init will get called multiple times, so you might be running Into an issue where the value is getting toggled (not clear from this snippet)

do as little work in the init as possible. If you want to set a value do it in onAppear or task modifier

@Sanjeev2 Could you provide the complete code snippet with an example of the data so that I can reproduce the issue.

Have you tried mapping the items outside ParentView?

Additionally, the ChildView should take a parameter of type [GroupedItems] but it currently takes a single GroupedItems instance hence the code doesn't compile.

Accepted Answer

init will get called multiple times, so you might be running Into an issue where the value is getting toggled (not clear from this snippet)

do as little work in the init as possible. If you want to set a value do it in onAppear or task modifier

Working code snippet:

import SwiftUI
import SwiftData

@main
struct MainApp: App {
    var body: some Scene {
        WindowGroup {
            SomeView()
        }
        .modelContainer(appContainer)
    }
}

struct SomeView: View {
    @Query private var items: [AItem]
    
    var body: some View {
        ParentView(items: items)
        
    }
}

struct ParentView: View {
    private var groupedItems: [GroupedAItems] = []
    
    init(items: [AItem]) {
        Dictionary(grouping: items) { $0.categoryName }
            .forEach {
                let groupedItems = GroupedAItems(categoryName: $0.key, items: $0.value)
                self.groupedItems.append(groupedItems)
            }
    }
    
    var body: some View {
        ScrollView {
            VStack(spacing: 15) {
                ForEach(groupedItems, id: \.self.categoryName) { groupedItems in
                    ChildView(groupedItems)
                }
            }
        }
    }
}

struct ChildView: View {
    public var groupedItems: GroupedAItems
    @State private var showItems: Bool
    
    init(_ groupedItems: GroupedAItems) {
        self.groupedItems = groupedItems
        self._showItems = State(initialValue: !groupedItems.completed)
        print("init, group \(groupedItems.categoryName) - items not completed \(!groupedItems.completed) - showItems \(showItems)")
    }
    
    var body: some View {
        print("body, group \(groupedItems.categoryName) - items not completed \(!groupedItems.completed) - showItems \(showItems)")
        if showItems {
            return AnyView(ItemsSampleView(items: groupedItems.items, onClick: { showItems = false }))
        } else {
            return AnyView(GroupsView(groupedItems: groupedItems, onClick: { showItems = true }))
        }
    }
}

struct ItemsSampleView: View {
    public var items: [AItem]
    public var onClick: () -> Void
    
    private let gridColumns = [GridItem(.adaptive(minimum: CGFloat(70)))]
    
    var body: some View {
        VStack {
            Button {
                onClick()
            } label: {
                Image(systemName: "chevron.down")
            }
            
            Spacer()
            
            LazyVGrid(columns: gridColumns) {
                ForEach(items.sorted(by: {$0.name < $1.name})) { item in
                    Button {
                        item.completed.toggle()
                    } label: {
                        Text(item.name)
                    }
                }
            }
        }
    }
}

struct GroupsView: View {
    public var groupedItems: GroupedAItems
    public var onClick: () -> Void
    
    var body: some View {
        VStack {
            Button {
                onClick()
            } label: {
                Image(systemName: "chevron.down")
            }
            
            Spacer()
            
            Text(groupedItems.categoryName)
        }
    }
}

@Model
final class AItem: Identifiable {
    @Attribute(.unique) public var id: String
    public var name: String
    public var categoryName: String
    public var completed = false
    
    internal init(name: String, categoryName: String) {
        self.id = UUID().uuidString
        self.name = name
        self.categoryName = categoryName
    }
}

struct GroupedAItems {
    var categoryName: String
    var items: [AItem]
    var completed: Bool {
        items.filter { !$0.completed }.isEmpty
    }
}

@MainActor
let appContainer: ModelContainer = {
    do {
        let container = try ModelContainer(for: AItem.self)
        
        // Make sure the persistent store is empty. If it's not, return the non-empty container.
        var itemFetchDescriptor = FetchDescriptor<AItem>()
        itemFetchDescriptor.fetchLimit = 1
        
        guard try container.mainContext.fetch(itemFetchDescriptor).count == 0 else { return container }
        
        container.mainContext.insert(AItem(name: "Apple", categoryName: "Fruits"))
        
        return container
    } catch {
        fatalError("Failed to create container")
    }
}()

Problem: When clicking on Apple item is (un-)completed. When it's completed then ChildView should show GroupsView. However, that's not the case.

The logs are like this:

init, group Fruits - items not completed false - showItems false
body, group Fruits - items not completed false - showItems true
init, group Fruits - items not completed false - showItems false
init, group Fruits - items not completed false - showItems false

body log is not called again.

SwiftUI view is not updated properly
 
 
Q