Open3

SwiftUI: ListのDividerが気に入らない件

kabeyakabeya

Listの項目間に自動で引かれるセパレータというかDividerですが、こいつの動きが気に入らないケースがあります。

例えば以下のようなケース。

リストの多くの項目が左寄せテキストで、一部、中央寄せボタンとかあるというようなパターンです。

Dividerは1個上の項目のテキストの頭から引かれているようで、中央寄せのボタンがあると、そこだけ左端が内側に入ったようになってしまいます。
このケースのソースは以下のような感じになります。

struct ContentView: View {
    var body: some View {
        List {
            Text("説明の題字")
                .font(.title)
            Text("そして説明の文言。\n長かったり、改行したり。")
            HStack {
                Spacer()
                Button(action: {}, label: {
                    Text("その上で何らかのボタン!")
                })
                .buttonStyle(.bordered)
                Spacer()
            }
            Text("※ それでもって、補足説明とか")
                .font(.footnote)
        }
    }
}

これを以下のように直したいわけです。

で、このDividerの位置調整は、.alignmentGuideモディファイアを使えばできるということです。

struct ContentView: View {
    var body: some View {
        List {
            Text("説明の題字")
                .font(.title)
            Text("そして説明の文言。\n長かったり、改行したり。")
            HStack {
                Spacer()
                Button(action: {}, label: {
                    Text("その上で何らかのボタン!")
                })
                .buttonStyle(.bordered)
                Spacer()
            }
            .alignmentGuide(.listRowSeparatorLeading) { _ in  0 } /// ← ここですね
            Text("※ それでもって、補足説明とか")
                .font(.footnote)
        }
    }
}

クロージャで0を返すと他のテキストと揃います。

kabeyakabeya

ただ、見た感じ、中途半端という雰囲気が否めないのも確かなので、いっそ幅いっぱいに引きたい、という話もあるでしょう。

つまり↓の感じです。

あんまり良い方法が分からなかったのですが、とりあえず以下のようにすればできます。

struct ContentView: View {
    var rowItemInsets = EdgeInsets(top: 8, leading: 20, bottom: 8, trailing: 20)
    
    var body: some View {
        List {
            Text("説明の題字")
                .font(.title)
                .listRowInsets(rowItemInsets)
                .alignmentGuide(.listRowSeparatorLeading) { _ in -rowItemInsets.leading}
            Text("そして説明の文言。\n長かったり、改行したり。")
                .listRowInsets(rowItemInsets)
                .alignmentGuide(.listRowSeparatorLeading) { _ in -rowItemInsets.leading}
            HStack {
                Spacer()
                Button(action: {}, label: {
                    Text("その上で何らかのボタン!")
                })
                .buttonStyle(.bordered)
                Spacer()
            }
            .listRowInsets(rowItemInsets)
            .alignmentGuide(.listRowSeparatorLeading) { _ in -rowItemInsets.leading}
            Text("※ それでもって、補足説明とか")
                .font(.footnote)
                .listRowInsets(rowItemInsets)
                .alignmentGuide(.listRowSeparatorLeading) { _ in -rowItemInsets.leading}
        }
    }
}

.listRowInsetsモディファイアはリストの各項目を親の枠からどんだけ引っ込めるかを設定する働きをします。
.alignmentGuideのクロージャの引数にインセットが入って来て良さそうなものなのですが、実際には幅・高さしか入っておらずインセットが分からないため、自分でインセットを指定しておいてその値を使います。

kabeyakabeya
struct ListRowFullWidthSeparatorModifier : ViewModifier {
    @ScaledMetric private var verticalInset: CGFloat = 8
    @ScaledMetric private var horizontalInset: CGFloat = 20
    
    func body(content: Content) -> some View {
        let rowItemInsets = EdgeInsets(top: self.verticalInset, leading: self.horizontalInset, bottom: self.verticalInset, trailing: self.horizontalInset)
        
        content
            .listRowInsets(rowItemInsets)
            .alignmentGuide(.listRowSeparatorLeading) { _ in -rowItemInsets.leading}
    }
}

extension View {
    func listRowFullWidthSeparator() -> some View {
        self.modifier(ListRowFullWidthSeparatorModifier())
    }
}

こんな感じのカスタムモディファイアを定義すると、以下のように書けます。

struct ContentView: View {
    var body: some View {
        List {
            Text("説明の題字")
                .font(.title)
                .listRowFullWidthSeparator()
            Text("そして説明の文言。\n長かったり、改行したり。")
                .listRowFullWidthSeparator()
            HStack {
                Spacer()
                Button(action: {}, label: {
                    Text("その上で何らかのボタン!")
                })
                .buttonStyle(.bordered)
                Spacer()
            }
            .listRowFullWidthSeparator()
            Text("※ それでもって、補足説明とか")
                .font(.footnote)
                .listRowFullWidthSeparator()
        }
    }
}

ちなみに@ScaledMetricは、「設定」App→「画面表示と明るさ」→「テキストサイズを変更」か、「設定」→「アクセシビリティ」→「画面表示とテキストサイズ」→「さらに大きな文字」でテキストサイズを変更したときに自動でスケーリングされる値です。

こうしておくと、文字のサイズを変えたときにリスト項目の間隔も自動で広がります。