How to remove section header top padding in SwiftUI Plain List with iOS16
Asked Answered
C

4

23

When we use SwiftUI List component and need a section header, top padding will appear on the header of each section.

Under iOS15, we use UITableView.appearance().sectionHeaderTopPadding = 0 to solve this problem. Check out attachmentiOS15&Xcode14 build screenshot.

But in iOS16, this setting doesn't seem to work. Check out attachmentiOS16&Xcode14 build screenshot, when scrolling: iOS16&Xcode14 build scroll.

So, my problem is how to remove section header top padding in iOS16, and when scrolling List, its section header will be pinned, just like attachment iOS15&Xcode14 build scroll.

Since iOS16, SwiftUI List component seems to use UICollectionView instead of UITableView, so I tried to solve this problem by setting the section top padding of UICollectionView globally. Unfortunately, I didn't find a way to set the section top padding of UICollectionView.

My code is below👇.

//
//  ContentView.swift
//  ListIniOS16
//
//  Created by Eric on 2022/07/23.
//

import SwiftUI

struct ContentView: View {
    @State private var collections: [ListData] = ListData.collection

    init() {
        let appBarTheme = UINavigationBarAppearance()
        appBarTheme.configureWithOpaqueBackground()
        appBarTheme.backgroundColor = UIColor(.gray)
        UINavigationBar.appearance().standardAppearance = appBarTheme
        UINavigationBar.appearance().scrollEdgeAppearance = appBarTheme
        if #available(iOS 15.0, *) {
            // can fix sectionHeaderTopPadding issue in iOS15,
            // but in iOS16 it doesn't work.
            UITableView.appearance().sectionHeaderTopPadding = 0
        }
    }
    
    var body: some View {
        NavigationView {
            VStack {
                List {
                    ForEach(collections) { item in
                        Section(header: SectionHeader(title: item.group)) {
                            ForEach(item.rows) { row in
                                Text(row.name)
                            }
                        }
                        .listRowBackground(Color.gray.opacity(0.3))
                    }
                }
                .listStyle(.plain)
            }
            .navigationTitle("ListSectionHeader")
            .navigationBarTitleDisplayMode(.inline)
        }
    }

    struct SectionHeader: View {
        let title: String

        var body: some View {
            ZStack(alignment: .leading) {
                Color(.lightGray)
                    .frame(maxWidth: .infinity)

                Text(title)
                    .font(.system(size: 16, weight: .bold))
                    .foregroundColor(.primary)
                    .padding(EdgeInsets(top: 10, leading: 18, bottom: 10, trailing: 18))
            }
            .listRowInsets(EdgeInsets())
        }
    }
}

struct ListData: Codable, Identifiable {
    var id = UUID()
    let group: String
    let rows: [Row]
}

extension ListData {
    static let collection: [ListData] = [.a, .b, .c]

    static let a = ListData(group: "A", rows: [.row1, .row2, .row3])

    static let b = ListData(group: "B", rows: [.row1, .row2, .row3, .row1, .row2, .row3])

    static let c = ListData(group: "C", rows: [.row1, .row2, .row3, .row1, .row2, .row3, .row1, .row2, .row3])
}

struct Row: Codable, Equatable, Identifiable {
    var id = UUID()
    let name: String
}

extension Row {
    static let row1 = Row(name: "Row1")
    static let row2 = Row(name: "Row2")
    static let row3 = Row(name: "Row3")
}

Many thanks.

Cammiecammy answered 3/9, 2022 at 13:36 Comment(1)
Hey @Eric. What solution did you chose to solve this problem?Dariadarian
C
5

You can remove the top padding from section headers in UICollectionViews like this:

var layoutConfig = UICollectionLayoutListConfiguration(appearance: .plain)
layoutConfig.headerMode = .supplementary
layoutConfig.headerTopPadding = 0
let listLayout = UICollectionViewCompositionalLayout.list(using: layoutConfig)
UICollectionView.appearance().collectionViewLayout = listLayout

Edit: Unfortunately I just saw that this overrides certain SwiftUI modifiers like .listRowSeparator() or .onDelete().

Catholicism answered 13/9, 2022 at 12:59 Comment(2)
Thanks for your solution. This solution solves part of the problem. As you mentioned, it overrides some settings of SwiftUI List. When we also use List without section header, it will cause the first row to be displayed repeatedly. In addition, there are some phenomena such as layout collapse after screen jumps.Cammiecammy
Warning: using this will break layout updates for lists and cause a crash, so I would not recommend it unless your list is guaranteed to be static.Tusche
P
3

You can find out the height of the header using GeometryReader than using negative padding move the list up. Something like this:

struct RootView: View {

@State var headerHeight = 0.0

var body: some View {
    List {
        Section {
            Text("11111")
            Text("22222")
            Text("22222")
            Text("22222")
            Text("22222")
            Text("22222")
            Text("22222")
            Text("22222")
        } header: {
            header
        }
        .listRowInsets(EdgeInsets())
    }
    .listStyle(GroupedListStyle())
    .padding(.top, -headerHeight)
}

private var header: some View {
    Color
        .clear
        .background(GeometryReader { geometryReader -> Color in
            DispatchQueue.main.async {
                print("\(geometryReader.frame(in: .local).height)")
                if headerHeight != geometryReader.frame(in: .local).height {
                    headerHeight = geometryReader.frame(in: .local).height
                }
            }
            return .clear
        })
}}
Puberty answered 26/10, 2022 at 9:43 Comment(0)
C
2

I had the same predicament that you had. At first I thought about using my headers as another cells, and while I did eliminate all spacing with .environment(\.defaultMinListRowHeight, 0), of course I didn't have pinned headers for scrolling. Ultimately, I replaced my List with a LazyVStack inside a ScrollView and adjusted my content to resemble how it looked before (with spacing, list separators, etc.).

ScrollView {
  LazyVStack(alignment: .leading, pinnedViews: [.sectionHeaders]) {
    Content()
  }
}
Chieftain answered 7/10, 2022 at 19:26 Comment(1)
Thanks for your comment. This does solve the header top padding problem, but when we chose to use ScrollView - LazyVStack to replace List, we found that because the types of rows are not uniform, we need to adjust the layout of each type of row, which requires breaking the code structure of the current project's row component, and the modification cost is a bit high, so we search for a lower cost solution continues.Cammiecammy
L
2

Although override the global UICollectionView.appearance() will fix this issue. But I recommend use the SwiftUI-Introspect to modify the SwiftUI component specifically.

Because some iOS internal collection view will be changed by this override. For example, on iOS 16 long press the textView the edit menu should be display but raise an exception. I have to remove the override to fix the crash problem.

List{
  …
}
.listStyle(.plain)
.introspect(.list, on: .iOS(.v16, .v17)) { collectionView in
  var configuration = UICollectionLayoutListConfiguration(appearance: .plain)
  configuration.headerMode = .supplementary
  configuration.headerTopPadding = .zero
  let layout = UICollectionViewCompositionalLayout.list(using: configuration)
  collectionView.setCollectionViewLayout(layout, animated: false)
}
Libradalibrarian answered 25/7, 2023 at 7:36 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.