1つのText
内で文字色を変えたり、太字にしたり、下線を入れたりと、文字装飾を行いたいことがあると思います。UIKit時代ではUILabel
やUITextView
とNSAttributedString
を利用して実装していたと思います。
今回は例として複数の 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) } }
としてあげれば
このように簡単に表示できます。もしタグが100件になったとしても。
このように問題なく表示できます。
さて、ここで「,
だけ色を赤くしてください」という要求が来たらどうしましょう。思いついた解決策は3つ。
- HStackにTextを積んでいく
- UIViewRepresentableを使って、UILabelとNSAttributedStringを使う
- Textを結合する
HStackにTextを積んでいく
まず1つめの「HStackにTextを積んでいく」ですが、これはダメでした。要素が少ない場合は良いのですが、HStackは折り返しに対応してないので、思った通りの挙動をしてくれません。
UIViewRepresentableを使って、UILabelとNSAttributedStringを使う
これも粘ったんですが、「高さがうまく算出できずに表示がぶっ壊れる」問題が解決できませんでした。
- SwiftUI: How to simulate NSAttributedString in SwiftUI ▪ Dreamcraft
- swiftui - How can I get text to wrap in a UILabel (via UIViewRepresentable) without having a fixed width? - Stack Overflow
- [SwiftUI]UIViewRepresentableのプレビューでsizeThatFitを有効にする - Qiita
これらの記事を参考に書いてみたコードがこちら
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) } } }
そしてプレビューがこちら
一見良さそうなんですが、 .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) } }
実行してあげると、
このように ,
を赤くすることができました。
まとめ
SwiftUIのTextは +
で結合できるので、UIKit時代は NSAttributedString
で行っていた文章内の色分けや下線・太字処理を簡単に実現できるようになりました。今回はタグをつなぎ合わせる ,
を赤くするだけの簡単なサンプルでしたが、例えば文章中のハッシュタグは青色にして太字にするといった処理も(データ形式によりますが)比較的簡単に書けるようになったと思います。