Eric
Eric

Reputation: 241

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

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.

Upvotes: 24

Views: 7935

Answers (4)

user25917
user25917

Reputation: 987

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)
}

Upvotes: 3

Claudiu
Claudiu

Reputation: 655

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
        })
}}

Upvotes: 3

Fercho_27
Fercho_27

Reputation: 31

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()
  }
}

Upvotes: 2

crazycrayfish
crazycrayfish

Reputation: 69

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().

Upvotes: 6

Related Questions