酢ろぐ!

カレーが嫌いなスマートフォンアプリプログラマのブログ。

iOS 16 の場合、SwiftUI の List で セクションヘッダー上部にマージンが入ってしまう

iOS 15 / iOS 16 の場合、SwiftUI の List で セクションヘッダーに top padding (top margin?) が入ってしまう問題がある。

ヘッダー上部にマージンが入る現象自体は iOS 15 の時にも発生していたが UITableView.appearance().sectionHeaderTopPadding = 0 で解決できた。iOS 16 からは List の内部実装が UITableView から UICollectionView に変わったようで、iOS 16 ではこの対策は使えなくなった。

この手の問題は、たとえば List から UITableView を取り出す SwiftUI-Introspect などを使えばもっと簡単に解決するのかもしれないが、Apple の都合で内部実装が変わってしまう現状を鑑みると適切な対応ではないだろう。

本記事ではそれっぽい対応策を記載しているが、iOS 15 の場合とは異なり実践では使えないかもしれない。

結論からいえば、過去のデザイン(iOS 14 での表示)に拘らずに、iOSのバージョンごとの SwiftUI でのデフォルト表示に任せるのが一番良いだろう。

iOS 15 で SwiftUI の List で セクションヘッダー上部にマージンが入ってしまう

Xcode 13 登場時に話題になった。SwiftUI だけの問題ではなく UITableView への変更だったので UITableViewController を継承した画面にも下記のようなコードをたくさん埋め込んだ。

    override func viewDidLoad() {
        super.viewDidLoad()
//...
        if #available(iOS 15.0, *) {
            tableView.sectionHeaderTopPadding = 0
        }
    }

一方、SwiftUI の List ではどのように対策したかといえば、前述したように UITableView.appearance().sectionHeaderTopPadding = 0 を実行して対策した。

struct ListView: View {
    
    var body: some View {
        List {
            Section {
                Text("🍎")
                Text("🍊")
                Text("🍓")
            } header: {
                Text("Red")
                    .listRowInsets(EdgeInsets())
                    .frame(maxWidth: .infinity)
                    .background(Color.red)
            }
            Section {
                Text("🥒")
                Text("🥬")
                Text("🫑")
            } header: {
                Text("Green")
                    .listRowInsets(EdgeInsets())
                    .frame(maxWidth: .infinity)
                    .background(Color.green)
            }
        }
        .navigationTitle("iOS \(UIDevice.current.systemVersion)")
        .navigationBarTitleDisplayMode(.inline)
        .listStyle(.plain)
        .onAppear {
            if #available(iOS 15.0, *) {
                UITableView.appearance().sectionHeaderTopPadding = 0
            }
        }
    }
}

iOS 16 で SwiftUI の List で セクションヘッダー上部にマージンが入ってしまう

単に見た目を合わせるだけであれば「How to remove section header top padding in SwiftUI Plain List with iOS16 - Stack Overflow」を参考にして対応することができる。

var layoutConfig = UICollectionLayoutListConfiguration(appearance: .plain)
layoutConfig.headerMode = .supplementary
layoutConfig.headerTopPadding = 0

let listLayout = UICollectionViewCompositionalLayout.list(using: layoutConfig)
UICollectionView.appearance().collectionViewLayout = listLayout

コード全体は以下の通り。

struct ListView: View {
    
    var body: some View {
        List {
            Section {
                Text("🍎")
                Text("🍊")
                Text("🍓")
            } header: {
                Text("Red")
                    .listRowInsets(EdgeInsets())
                    .frame(maxWidth: .infinity)
                    .background(Color.red)
            }
            Section {
                Text("🥒")
                Text("🥬")
                Text("🫑")
            } header: {
                Text("Green")
                    .listRowInsets(EdgeInsets())
                    .frame(maxWidth: .infinity)
                    .background(Color.green)
            }
        }
        .navigationTitle("iOS \(UIDevice.current.systemVersion)")
        .navigationBarTitleDisplayMode(.inline)
        .listStyle(.plain)
        .onAppear {
            if #available(iOS 16.0, *) {
                var layoutConfig = UICollectionLayoutListConfiguration(appearance: .plain)
                layoutConfig.headerMode = .supplementary
                layoutConfig.headerTopPadding = 0

                let listLayout = UICollectionViewCompositionalLayout.list(using: layoutConfig)
                UICollectionView.appearance().collectionViewLayout = listLayout
            } else if #available(iOS 15.0, *) {
                UITableView.appearance().sectionHeaderTopPadding = 0
            }
        }
    }
}

懸念点

ただこの方法はあまり筋がよくない。たとえば SmartNews のような上部タブがあるアプリの場合、上部タブはおそらく UICollectionView を使って実装されている。UICollectionView.appearance() に対しての変更処理をおこなっているので、競合をおこして上部タブの表示が狂ってしまうだろう。