2

Issue After upgrading to Xcode 26 and iPadOS26 I recognized that if I am using the SwiftUI Table-component and I scroll the content, the content will flow behind the header and is still visible. It seems like the header have no background at all.

What I tried

  • Using custom table headers
  • Setting background modifier
  • Trying scrollContentBackground
  • Trying .list* modifiers

Question Do you have any solution for this problem? It seems to be "new" in iPadOS26 with enabled glass.

2
  • Have you tried setting .scrollEdgeEffectStyle(.hard, for: .all)? Commented Oct 22 at 17:08
  • @AndreiG. thanks, I tried it now, but no effects / fix is visible Commented Oct 23 at 7:13

1 Answer 1

2

It seems amazing that Apple doesn't provide some way to style the header row of a table, or at least, an option to prevent the rows from being visible when scrolling behind the header.

As a workaround, you could try showing an empty table as an overlay over the populated table. The empty table will consist of a header row only. Then fill the background of the overlay, to mask out the scrolled rows.

Some notes:

  • Apply the overlay using alignment: .top.
  • Apply .scrollContentBackground(.hidden) to the empty table, to hide the default background.
  • Use a solid color behind the overlay, to mask out the scrolled rows.
  • By default, a table uses all the vertical space available, so the height of the background needs to be restricted to the expected height of the header.
  • The expected height of the header can be defined as a ScaledMetric, so that it adapts to different text sizes.
  • Apply .allowsHitTesting(false) to the overlay, to allow gestures to pass through to the (populated) table below.

This approach relies on the widths of the columns for an empty table being the same as the widths of the colums for a populated table. This does appear to be the case.

The example below shows it working. This is an adaption of the example provided on the reference page for Table:

struct ContentView: View {
    @ScaledMetric(relativeTo: .title) private var headerHeight: CGFloat = 40
    @State private var people = [
        Person(givenName: "Juan", familyName: "Chavez", emailAddress: "[email protected]"),
        Person(givenName: "Mei", familyName: "Chen", emailAddress: "[email protected]"),
        Person(givenName: "Tom", familyName: "Clark", emailAddress: "[email protected]"),
        Person(givenName: "Gita", familyName: "Kumar", emailAddress: "[email protected]")
    ]

    @TableColumnBuilder<Person, KeyPathComparator<Person>>
    private func columns() -> some TableColumnContent<Person, KeyPathComparator<Person>> {
        TableColumn("Given Name", value: \.givenName)
        TableColumn("Family Name", value: \.familyName)
        TableColumn("E-Mail Address", value: \.emailAddress)
    }

    var body: some View {
        Table(people, columns: columns)
            .overlay(alignment: .top) {
                Table([Person](), columns: columns)
                    .scrollContentBackground(.hidden)
                    .background(alignment: .top) {
                        Rectangle()
                            .fill(.background)
                            .ignoresSafeArea()
                            .frame(height: headerHeight)
                    }
                    .allowsHitTesting(false)
            }
    }
}

struct Person: Identifiable {
    let givenName: String
    let familyName: String
    let emailAddress: String
    let id = UUID()
    var fullName: String { givenName + " " + familyName }
}

Animation


EDIT One small problem with the solution above is that the table can be pulled down, which reveals the (regular) header row for the full table underneath.

If the column headers are not interactive (that is, if neither sorting nor column customization is supported) then a workaround for this issue is to hide the header row for the full table completely. Then add top padding, to make space for the overlay:

Table(people, columns: columns)
    .tableColumnHeaders(.hidden)
    .padding(.top, headerHeight)
    .overlay(alignment: .top) {
        // ... as before
    }

Animation

Alternatively, .onScrollGeometryChange can be used to measure the scroll offset. If it is negative, the negated offset can be applied to the overlay so that it moves in sync:

@State private var scrollOffset = CGFloat.zero
Table(people, columns: columns)
    .onScrollGeometryChange(for: CGFloat.self) { proxy in
        proxy.contentOffset.y + proxy.contentInsets.top
    } action: { _, offset in
        scrollOffset = offset
    }
    .overlay(alignment: .top) {
        Table([Person](), columns: columns)
            .scrollContentBackground(.hidden)
            .background(alignment: .top) {
                Rectangle()
                    .fill(.background)
                    .ignoresSafeArea()
                    .frame(height: headerHeight)
            }
            .offset(y: max(0, -scrollOffset))
            .allowsHitTesting(false)
    }

Animation

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.