🎯

SwiftUIで箇条書きを実現する

2023/08/17に公開

UIKitで箇条書きを実現する際、NSParagraphStyleheadIndentを使ってNSAttributedStringを整形することになります(本記事ではやり方については触れません)。
しかし、SwiftUIでは上記に該当するAPIがありません[1]

試しに、普通に複数行のテキストを箇条書きっぽく実装してみます。

VStack(spacing: 4) {
    Text("Hello world!")
        .font(.title)
        .frame(maxWidth: .infinity, alignment: .leading)
    // 通常テキスト
    Text("""
        ・foo
        ・Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt  mollit anim id est laborum.
        ・bar
        """)
    // Markdown形式
    Text(.init("""
        - [foo](https://foo.bar)
        - Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt  mollit anim id est laborum.
        - **bar**
        """))
}
.font(.body)
.padding()
.background {
    Color.gray
}

結果は次の通りです。

Before

Markdown形式も試してみましたが、SwiftUIのMarkdownが箇条書きに未対応[2]のため正しく反映されていないことが分かります。

そこで、試行錯誤の結果LabeliconにImage以外もセットできることからiconに箇条書きのドット()を埋め込んでみました。

VStack(spacing: 4) {
    Text("Hello world!")
        .font(.title)
        .frame(maxWidth: .infinity, alignment: .leading)
    Label {
        Text("foo")
    } icon: {
        Text("・")
    }.frame(maxWidth: .infinity, alignment: .leading)
    Label {
        Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.")
    } icon: {
        Text("・")
    }.frame(maxWidth: .infinity, alignment: .leading)
    Label {
        Text("bar")
    } icon: {
        Text("・")
    }.frame(maxWidth: .infinity, alignment: .leading)
}.font(.body)
.padding()
.background {
    Color.gray
}

結果は以下の通りです。

After

それっぽく見えるようになったことが分かります。

ただ、これを毎回書くと面倒なのでまとめちゃいますw

public struct BulletPointsText: View {
    var items: [AttributedString]
    var spacing: CGFloat
    var bullet: String

    public init(bullet: String = "・",
                spacing: CGFloat = 4,
                _ items: [AttributedString]) {
        self.items = items
        self.spacing = spacing
        self.bullet = bullet
    }

    public init(bullet: String = "・",
                spacing: CGFloat = 4,
                _ items: [String]) {
        self.items = items.map(AttributedString.init)
        self.spacing = spacing
        self.bullet = bullet
    }

    public var body: some View {
        VStack(spacing: spacing) {
            ForEach(items, id: \.self) { item in
                Label {
                    Text(item)
                } icon: {
                    Text(bullet)
                }
                .frame(maxWidth: .infinity, alignment: .leading)
            }
        }
    }
}

public extension BulletPointsText {
    init(bullet: String = "・",
         spacing: CGFloat = 4,
         _ items: AttributedString...) {
        self.init(bullet: bullet,
                  spacing: spacing,
                  items)
    }

    init(bullet: String = "・",
         spacing: CGFloat = 4,
         _ items: String...) {
        self.init(bullet: bullet,
                  spacing: spacing,
                  items)
    }
}
BulletPointsText(
    "foo",
    "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
    "bar"
)

これである程度スッキリ書けるようになったかなと思います。

ただし、iOS16以上ならGridViewが使えるのでそれで実現できるかもしれません。

脚注
  1. 2023年8月17日現在 ↩︎

  2. https://www.yururiwork.net/archives/1856 ↩︎

Discussion