文字っぽいの。

文字を書いています。写真も混ざります。

SwiftUIでUILabelやNSAttributedStringを利用せずに文字装飾をがんばる

1つのText 内で文字色を変えたり、太字にしたり、下線を入れたりと、文字装飾を行いたいことがあると思います。UIKit時代ではUILabelUITextViewNSAttributedStringを利用して実装していたと思います。

今回は例として複数の tag, で結合して表示したい場合考えていきます。さて、なにも装飾せずに結合して表示するのはとても簡単です。

struct ContentView: View {
    let tags = Array(repeating: "tag", count: 10)

    var body: some View {
        Text(tags.joined(separator: ", "))
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView().previewLayout(.sizeThatFits)
    }
}

としてあげれば

f:id:FromAtom:20200510225845p:plain

このように簡単に表示できます。もしタグが100件になったとしても。

f:id:FromAtom:20200510225946p:plain

このように問題なく表示できます。

さて、ここで「,だけ色を赤くしてください」という要求が来たらどうしましょう。思いついた解決策は3つ。

  • HStackにTextを積んでいく
  • UIViewRepresentableを使って、UILabelとNSAttributedStringを使う
  • Textを結合する

HStackにTextを積んでいく

まず1つめの「HStackにTextを積んでいく」ですが、これはダメでした。要素が少ない場合は良いのですが、HStackは折り返しに対応してないので、思った通りの挙動をしてくれません。

f:id:FromAtom:20200510230728p:plain

UIViewRepresentableを使って、UILabelとNSAttributedStringを使う

これも粘ったんですが、「高さがうまく算出できずに表示がぶっ壊れる」問題が解決できませんでした。

これらの記事を参考に書いてみたコードがこちら

struct TextWithAttributedString: UIViewRepresentable {
    typealias UIViewType = UILabel

    var width: CGFloat
    var attributedString: NSAttributedString

    func makeUIView(context: Context) -> UILabel {
        let label = UILabel()
        label.numberOfLines = 0
        label.attributedText = attributedString
        label.lineBreakMode = .byTruncatingTail
        label.preferredMaxLayoutWidth = width
        label.setContentHuggingPriority(.defaultHigh, for: .horizontal)
        label.setContentHuggingPriority(.defaultHigh, for: .vertical)

        return label
    }

    func updateUIView(_ uiView: UILabel, context: Context) {
        uiView.attributedText = attributedString
        uiView.setContentHuggingPriority(.defaultHigh, for: .horizontal)
        uiView.setContentHuggingPriority(.defaultHigh, for: .vertical)
    }

}

struct TextWithAttributedString_Previews: PreviewProvider {
    static var previews: some View {
        let tags = Array<String>(repeating: "tag", count: 100)
        let attributedString = NSAttributedString(string: tags.joined(separator: ", "))

        return Group {
            GeometryReader { geometry in
                TextWithAttributedString(width: geometry.size.width, attributedString: attributedString)
                .fixedSize()
            }.previewLayout(.sizeThatFits)
        }
    }
}

そしてプレビューがこちら

f:id:FromAtom:20200510231821p:plain

一見良さそうなんですが、 .sizeThatFits しているのに無駄な高さが発生してしまっています。もちろん、ここで実装した TextWithAttributedString を別のView内で使うと高さ計算がおかしくなっており、表示が重なったり無駄な空白ができてしまいます。

執筆時に思いついたんですが、UILabelではなくてUITextViewを利用すれば、うまくいくかもしれません。

Textを結合する

最後の方法です。実はSwiftUIの Text+ でつなげていくことができます。つまり、

struct ContentView: View {
    var body: some View {
        Text("A") + Text("B") + Text("C")
    }
}

という書き方が可能です。この機能を利用して実装してみたのがこちら、

struct ContentView: View {
    let tags = Array(repeating: "tag", count: 30) + ["lastTag"]

    var body: some View {
        tags
            .dropLast()
            .map { Text($0) + Text(",").foregroundColor(.red) }
            .reduce(Text("")) { $0 + $1 }
            + Text(tags.last ?? "")
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        return ContentView().previewLayout(.sizeThatFits)
    }
}

実行してあげると、

f:id:FromAtom:20200510234536p:plain

このように , を赤くすることができました。

まとめ

SwiftUIのTextは + で結合できるので、UIKit時代は NSAttributedString で行っていた文章内の色分けや下線・太字処理を簡単に実現できるようになりました。今回はタグをつなぎ合わせる , を赤くするだけの簡単なサンプルでしたが、例えば文章中のハッシュタグは青色にして太字にするといった処理も(データ形式によりますが)比較的簡単に書けるようになったと思います。