SwiftUIでルビを振るために必要なこと

# 概要
ルビを振ったUILabelをSwiftUIから利用する実装についてまとめました。

【参考】
https://qiita.com/mlmlykt/items/c8a09d6801b5aac7f24e
https://qiita.com/negi0205/items/6c73128ff2cf680df47c
https://qiita.com/woxtu/items/284369fd2654edac2248

# ルビを振ったNSAttributedStringの生成
```swift
extension String {
    func createRuby(color: UIColor = .black) -> NSAttributedString {
        let textWithRuby = replacingOccurrences(of: "(｜.+?《.+?》)", with: ",$1,", options: .regularExpression)
            .components(separatedBy: ",")
            .map { component -> NSAttributedString in
                guard component.range(of: "｜(.+?)《(.+?)》", options: .regularExpression, range: nil, locale: nil) != nil else {
                    return NSAttributedString(string: component)
                }
                
                let baseText = component.replacingOccurrences(of: "｜(.+?)《.+?》", with: "$1", options: .regularExpression)
                let rubyText = component.replacingOccurrences(of: "｜.+?《(.+?)》", with: "$1", options: .regularExpression)

                let rubyAttribute: [CFString: Any] = [
                    kCTRubyAnnotationSizeFactorAttributeName: 0.5,
                    kCTForegroundColorAttributeName: color
                ]
                // 必要に応じて属性を設定する
                let rubyAnnotation = CTRubyAnnotationCreateWithAttributes(
                    .auto, .auto, .before, rubyText as CFString, rubyAttribute as CFDictionary
                )
                
                return NSAttributedString(string: baseText,
                                          attributes: [kCTRubyAnnotationAttributeName as NSAttributedString.Key: rubyAnnotation])
            }
            .reduce(NSMutableAttributedString()) {
                $0.append($1)
                return $0
            }
        
        return textWithRuby
    }
}
```
`UILabel`の`attributedText`に設定するルビ付き文字列は`CTRubyAnnotationCreateWithAttributes`を使った`NSAttributedString`によって実現できます。
`CTRubyAnnotationCreateWithAttributes`については[公式ドキュメント](https://developer.apple.com/documentation/coretext/1642000-ctrubyannotationcreatewithattrib)か上記の[参考記事](https://qiita.com/mlmlykt/items/c8a09d6801b5aac7f24e)あたりを見ながら必要に応じて設定してください。
調べてみると参考記事では`NSRegularExpression`を使うコードが多いようですが、普通に`String`から該当箇所を抽出するコードの方が新しい書き方になるかなと思ってそのように修正してあります。
ルビの振り方のルールについては参考記事を踏襲してあります。

# ルビを振ったUILabelの生成
```swift
class RubyLabel: UILabel {
    override func drawText(in rect: CGRect) {
        guard let context = UIGraphicsGetCurrentContext() else { return }
        context.translateBy(x: 0, y: rect.height)
        context.scaleBy(x: 1.0, y: -1.0)

        guard let attributedText = attributedText else { return }
        let frame = CTFramesetterCreateFrame(
            CTFramesetterCreateWithAttributedString(attributedText),
            CFRangeMake(0, attributedText.length),
            CGPath(rect: rect, transform: nil),
            nil
        )
        CTFrameDraw(frame, context)
    }
    
    override var intrinsicContentSize: CGSize {
        let size = super.intrinsicContentSize
        let intrinsicContentWidth = size.width
        guard let attributedText = attributedText else {
            let newHeight = size.height * 1.5
            return CGSize(width: intrinsicContentWidth, height: newHeight)
        }
        // 適切な高さになるように計算する
        let setter = CTFramesetterCreateWithAttributedString(attributedText)
        let range = CFRange()
        let constraints = CGSize(width: intrinsicContentWidth, height: CGFloat.greatestFiniteMagnitude)
        let frameSize = CTFramesetterSuggestFrameSizeWithConstraints(setter, range, nil, constraints, nil)
        return CGSize(width: intrinsicContentWidth, height: frameSize.height)
    }
}
```
参考記事では説明がないですが、以下のコード部分については説明が必要かなと思うので説明しておきます。

```swift
context.translateBy(x: 0, y: rect.height)
context.scaleBy(x: 1.0, y: -1.0)
```
[CGRectの公式ドキュメント](https://developer.apple.com/documentation/coregraphics/cgrect)を読むと、Core Graphicsの座標系はデフォルトでは原点が左下であるという記載があります。
しかし、ご存じの通りiOSの世界（UIKit）では通常座標系の原点は左上になります。
つまり、`UILabel`を継承してdrawする処理において、`UIGraphicsGetCurrentContext`で得られた`CGContext`は左上が原点の座標系になると思われます。
一方で、`CTFrameDraw`での描画では特に何の指定もなければiOSの世界に紐づくわけでもないので、左下が原点の座標系になります。
なので、`CTFrameDraw`を使ってCore Textを描画する場合はy軸を反転させる必要がある、ということから上記のコードが必要になると思われます。
参考記事：https://qiita.com/wheel/items/a26f7cb1464aff60940a

続いて、`intrinsicContentSize`を算出する処理が必要になります。
参考記事ではルビを振った`UILabel`の高さを1.5倍の高さにしているコードしか見当たらなかったのですが、これは`kCTRubyAnnotationSizeFactorAttributeName`で指定したルビの高さ分だけ高くしていると思われます。
しかし、単に1.5倍の高さにしてしまうと、複数行あるテキストのうち1行だけルビが振ってあるようなケースで高さ計算が狂ってしまいます。
そこで`intrinsicContentSize`は以下のコードによって適切な高さを算出して返却するようにしています。

```swift
let setter = CTFramesetterCreateWithAttributedString(attributedText)
let range = CFRange()
let constraints = CGSize(width: intrinsicContentWidth, height: CGFloat.greatestFiniteMagnitude)
let frameSize = CTFramesetterSuggestFrameSizeWithConstraints(setter, range, nil, constraints, nil)
return CGSize(width: intrinsicContentWidth, height: frameSize.height)
```

`CTFramesetterSuggestFrameSizeWithConstraints`についてはあまり参考になる記事もなかったので、[公式のドキュメント](https://developer.apple.com/documentation/coretext/1478566-ctframesettersuggestframesizewit)を見るのが一番わかりやすいと思います。

```swift
framesetter: frameの測定対象となるsetter
stringRange: 適用される文字列の範囲。範囲の長さが0の場合はテキストまたはスペースがなくなるまで行を追加し続ける（なので全文適用したい場合は0指定しておけばよさそう）
frameAttributes: frameの塗りつぶしプロセスを制御する追加属性
constraints: frameのサイズ制約。width/heightのどちらかにCGFLOAT_MAXを設定すれば制約なしとして扱われる（と記載があるが、CGFLOAT_MAXはdeprecatedなのでCGFloat.greatestFiniteMagnitudeを使う）
fitRange: ポインタで返ってくる返り値。実際に制約されたサイズに収まる文字列の範囲が返却される
```

# ルビを振ったSwiftUIのViewの生成
```swift
struct RubyLabelRepresentable: UIViewRepresentable {
    let attributedText: NSAttributedString
    let font: UIFont
    let textColor: UIColor
    let textAlignment: NSTextAlignment
    
    private let rubyLabel = RubyLabel()

    func makeUIView(context: Context) -> UILabel {
        rubyLabel.numberOfLines = 0
        rubyLabel.setContentHuggingPriority(.required, for: .horizontal)
        rubyLabel.setContentHuggingPriority(.required, for: .vertical)
        return rubyLabel
    }

    func updateUIView(_ uiView: UILabel, context: Context) {
        rubyLabel.attributedText = attributedText
        rubyLabel.font = font
        rubyLabel.textColor = textColor
        rubyLabel.textAlignment = textAlignment
    }
}
```

基本的には、`RubyLabel`を`UIViewRepresentable`でラップするだけになります。
使うときに必要になるパラメータは全て引数としてセットできるようにしておくと便利です。
使うときにサイズ指定しなくても済むようにするためには以下の記述が必要になります。

```swift
rubyLabel.setContentHuggingPriority(.required, for: .horizontal)
rubyLabel.setContentHuggingPriority(.required, for: .vertical)
```

この記述はViewの大きくなりにくさを示すPriorityで、私の場合は縦横ともに可能な限り自身のサイズを優先するように指定しています。
あとは`numberOfLines`を0に設定しておけば複数行にも対応できていい感じです。
このViewを使う場合は以下のように呼び出せば利用可能です。

```swift
RubyLabelRepresentable(
  attributedText: "|試《ため》しの|文字列《もじれつ》".createRuby(),
  font: .systemFont(ofSize: 24),
  textColor: .black,
  textAlignment: .center
)
```