9
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

朝日新聞社Advent Calendar 2024

Day 24

【SwiftUI】iOS 15でListのSection上部に余白が生じないようプリミティブに対処する

Last updated at Posted at 2024-12-23

問題

SwiftUIでListを表示させると、iOS 15では最初のSectionの上部に余白が生じてしまう。

スクリーンショット

iOS 16.0(正常) iOS 15.5(異常)
13pro_16_0.png 13pro_15_5.png
【参考】上記画面のコード
import SwiftUI

struct ContentView: View {
    var body: some View {
        NavigationView {
            List {
                Section("セクション1") {
                    Text("メニュー1")
                    Text("メニュー2")
                    Text("メニュー3")
                    Text("メニュー4")
                    Text("メニュー5")
                }
                Section("セクション2") {
                    Text("メニュー1")
                    Text("メニュー2")
                    Text("メニュー3")
                    Text("メニュー4")
                    Text("メニュー5")
                }
            }
            .navigationBarTitle("タイトル")
        }
    }
}

#Preview {
    ContentView()
}

上述の余白は、.listStyle(.plain)を適用したListであれば、UITableView.appearance().sectionHeaderTopPaddingを0に設定することで解消される可能性が高い1。しかし、デフォルトの.listStyle(.insetGrouped)が適用されたListではこの設定が効かなかった。さらに、iOS 16以降では仕様が変わり、この解決策は機能しないようである2。他に、SwiftUIIntrospectを用いてUIKitの側をカスタマイズするなど、Webで公開されている一通りの方法を試したものの、解消には至らなかった。

そこで、スクラッチで新たにListを定義してみたい。プリミティブであり、エレガントな解決法とは言えないが、OSバージョン間の表示の差異を無くすためには確実で分かりやすいと思われたからである。

方針

  • ViewBuilderを用いて、Listに相当する要素(以下、カスタムListと称する)、Sectionに相当する要素、及びSection内の項目用の要素を新たに定義する
  • ViewExtractor を用いて、カスタムList内のコンテンツを順に読み込み、表示させる
    • その際、最初と最後の要素のみ部分的に角丸を適用する
      • 最初の要素は上辺、最後の要素は下辺のみ角丸に
    • 上記以外の要素の場合、あるいはコンテンツの数が一つの場合は、全ての角を丸くする
  • ヘッダーをコンテンツの上に表示させる
    • ヘッダーの中身はカスタムListのオプションで指定できるようにする

解決

コード

※ 部分的な角丸の実装は、「『一部だけ角丸にする』をSwiftUIで実現する」(https://qiita.com/chocoyama/items/1cb7040f0e717406a6f4) を参照されたい。

import SwiftUI
import ViewExtractor

struct ListItemResetModifier: ViewModifier {
    func body(content: Content) -> some View {
        content
            .listRowInsets(EdgeInsets())
            .listRowBackground(Color.clear)
    }
}

struct CustomList<T: View>: View {
    @ViewBuilder var content: () -> T
    var body: some View {
        List {
            content()
        }
        .environment(\.defaultMinListRowHeight, 0)
    }
}

struct CustomSection<T: View>: View {
    var header: String?
    var cornerRadius: CGFloat = 8
    @ViewBuilder let content: T
    var body: some View {
        Group {
            ListHeader(header)
            ExtractMulti(content) { views in
                ForEach(views) { view in
                    if views.count > 1 {
                        if view.id == views.first?.id {
                            view.cornerRadius(cornerRadius, maskedCorners: [.layerMaxXMinYCorner, .layerMinXMinYCorner])
                                .modifier(ListItemResetModifier())
                        } else if view.id == views.last?.id {
                            view.cornerRadius(cornerRadius, maskedCorners: [.layerMaxXMaxYCorner, .layerMinXMaxYCorner])
                                .modifier(ListItemResetModifier())
                                .listRowSeparator(.hidden, edges: .bottom)
                        } else {
                            view.modifier(ListItemResetModifier())
                        }
                    } else {
                        view.modifier(ListItemResetModifier())
                            .cornerRadius(cornerRadius)
                            .listRowSeparator(.hidden)
                    }
                }
            }
        }
        VStack {}
            .frame(height: 32)
            .modifier(ListItemResetModifier())
    }
}

struct ListHeader: View {
    var text: String?
    init(_ text: String?) {
        self.text = text
    }
    var body: some View {
        if let text = text {
            Text(text)
                .font(.custom("HiraginoSans-W3", fixedSize: 12))
                .foregroundStyle(Color.secondary)
                .padding(.horizontal, 20)
                .padding(.vertical, 4)
                .frame(maxWidth: .infinity, alignment: .leading)
                .listRowInsets(EdgeInsets())
                .listRowBackground(Color.clear)
                .listRowSeparator(.hidden)
        }
    }
}

struct ListItem<T: View>: View {
    @ViewBuilder var content: () -> T
    var body: some View {
        HStack {
            content()
        }
        .padding(.vertical, 0)
        .padding(.horizontal, 16)
        .frame(maxWidth: .infinity, minHeight: 44, alignment: .leading)
        .background(Color.white)
        .listRowSeparator(.hidden)
    }
}

struct ContentView: View {
    var body: some View {
        NavigationView {
            CustomList {
                CustomSection(header: "セクション1") {
                    ListItem {
                        Text("メニュー1")
                    }
                    ListItem {
                        Text("メニュー2")
                    }
                    ListItem {
                        Text("メニュー3")
                    }
                    ListItem {
                        Text("メニュー4")
                    }
                    ListItem {
                        Text("メニュー5")
                    }
                }
                CustomSection(header: "セクション2") {
                    ListItem {
                        Text("メニュー1")
                    }
                    ListItem {
                        Text("メニュー2")
                    }
                    ListItem {
                        Text("メニュー3")
                    }
                    ListItem {
                        Text("メニュー4")
                    }
                    ListItem {
                        Text("メニュー5")
                    }
                }
            }
            .environment(\.defaultMinListRowHeight, 0)
            .navigationBarTitle("タイトル")
        }
    }
}


#Preview {
    ContentView()
}

スクリーンショット

iOS 16.0 iOS 15.5
after_16_0.png after_15_5.png

上記のコードによって、バージョン間の表示の差が解消されたことが分かる。

  1. 詳しくは 「【SwiftUI】iOS15でListのSection上部にスペースが生じる件」(https://www.2nd-walker.com/2021/09/29/swiftui-remove-top-padding-of-list-in-ios15/) を参照されたい。

  2. 詳しくは「【SwiftUI】iOS16でまたListのSection上部にスペースが生じた件」(https://www.2nd-walker.com/2022/09/14/swiftui-list-has-extra-top-padding-again-in-ios16/) を参照されたい。

9
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?