swiftuitoolbarswiftui-list

SwiftUI Lists Section - Add a header and the toolbar items repeat


I am displaying a list of names in a “list". When I encapculate this in a section it works. BUT when I add the header the Toolbar items get duplicated. If I REM it out it behaves as expected, e.g.:

/* header: {
     Text("Names in Order")
     .frame(width: 380, alignment: .leading)
} */

Examples of the screen shots and code follows:

Normal Toolbar Toolbar Items Duplicated

import SwiftUI
import SwiftData

enum AppearanceStyle {
    case dark
    case light
    case auto
}

struct EntrantsHomeView: View {
    @Environment(\.modelContext) var context
    @State private var isShowingItemSheet = false
    @Query(sort: \EntrantsDatabase.number) var entrants: [EntrantsDatabase]
    @State private var entrantToEdit: EntrantsDatabase?
    @State private var appearance: AppearanceStyle = .auto
    
    
    var body: some View {
        Section {
            List {
                ForEach(entrants) { entrant in
                    EntrantCell(entrant: entrant)
                        .onTapGesture {
                            entrantToEdit = entrant
                        }
                }
                .onDelete { indexSet in
                    for index in indexSet {
                        context.delete(entrants[index])
                        try! context.save()
                    }
                }
            }
        } /* header: {
            Text("Names in Order")
            .frame(width: 380, alignment: .leading)
        } */
        .overlay {
            if entrants.isEmpty {
                ContentUnavailableView(label: {
                    Label("No Names", systemImage: "list.bullet.rectangle.portrait")
                }, description: {
                    Text("Start adding names to your list.")
                }, actions: {
                    Button("Add Name") {isShowingItemSheet = true }
                })
                .offset(y: -60)
            }
        }
        .toolbar(content: {
            if !entrants.isEmpty {
                ToolbarItem(placement: .topBarLeading) {
                    Button("Delete Name", systemImage: "minus") {
                        do {
                            try context.delete(model: EntrantsDatabase.self)
                        } catch {
                            print("Failed to clear the Names database")
                        }
                    }
                }
                ToolbarItem(placement: .topBarTrailing) {
                    Button("Add Name", systemImage: "plus") {
                        isShowingItemSheet = true
                    }
                }
            }
        })
        .navigationTitle("Names' Manager")
        .navigationBarTitleDisplayMode(.large)
        .sheet(isPresented: $isShowingItemSheet) {AddEntrantSheet() }
        .sheet(item: $entrantToEdit) { entrant in
            UpdateEntrantSheet(entrant: entrant)
        }
    }
}


struct EntrantCell: View {
    let entrant: EntrantsDatabase
    var body: some View {
        HStack {
            CircledText(text: String(entrant.number))
            Spacer()
            Text(entrant.name)
                .frame(width: 160, alignment: .leading)
                .bold()
            Spacer()
        }
    }
}

Solution

  • A Section with a header may look like one view, but it's actually two views. Compare:

    var body: some View {
        Group(subviews: twoViews) { subviews in
            Text("\(subviews.count)") // 2
        }
        Group(subviews: section) { subviews in
            Text("\(subviews.count)") // 2
        }
    
        // also see:
        ZStack { section }
    }
    
    @ViewBuilder var section: some View {
        Section {
            Text("Foo")
        } header: {
            Text("Bar")
        }
    }
    @ViewBuilder var twoViews: some View {
        Text("Foo")
        Text("Bar")
    }
    

    Similarly, a Section with both a header and a footer is three views.

    As a result, the toolbar modifier modifies the two views at the same time, creating two copies of the same toolbar.

    To fix this, you can move the toolbar modifier to modify the List instead.

    The same applies to the overlay and sheet modifiers, so I suggest you move them to the List as well.

    Alternatively, use a VStack instead of Section. VStack counts as one view no matter how many views it contain. Only certain views give Sections special appearances (e.g. List). Your Section doesn't seem to be in such a view, so I'd suggest just using a VStack instead.