【Swift4】CTRubyAnnotationとNSAttributedStringでルビを振る

# はじめに
`NSAttributedString`を利用したルビの実装についてです。
以下の記事を参考にさせていただきました。
https://qiita.com/woxtu/items/284369fd2654edac2248
こちらに則って、青空文庫形式での実装になります。
例：「デーモン｜小暮閣下《こぐれかっか》」

# ルビ判別のための正規表現
青空文庫形式自体を判別するための正規表現を`extension`で実装しました。
ほぼ参考記事のままとなります。

```swift:String+Ruby.swift
func find(pattern: String) -> NSTextCheckingResult? {
    do {
        let findRubyText = try NSRegularExpression(pattern: pattern, options: [])
        return findRubyText.firstMatch(
            in: self,
            options: [],
            range: NSMakeRange(0, self.utf16.count))
    } catch {
        return nil
    }
}

func replace(pattern: String, template: String) -> String {
    do {
        let replaceRubyText = try NSRegularExpression(pattern: pattern, options: [])
        return replaceRubyText.stringByReplacingMatches(
            in: self,
            options: [],
            range: NSMakeRange(0, self.utf16.count),
            withTemplate: template)
    } catch {
        return self
    }
}
```

# ルビ付きのNSAttributedStringを作る
ルビ付きの`NSAttributedString`を作る際は、
Keyが`kCTRubyAnnotationAttributeName`、Valueが`CTRubyAnnotation`の`attribute`を追加する必要があります。
https://developer.apple.com/documentation/coretext/ctrubyannotation
今回はStringのextensionとして実装しています。
こちらもほぼ参考記事のままとなっています。

```swift:String+Ruby.swift
func rubyAttributedString(font: UIFont, textColor: UIColor) -> NSMutableAttributedString {
    let attributed =
        self.replace(pattern: "(｜.+?《.+?》)", template: ",$1,")
            .components(separatedBy: ",")
            .map { x -> NSAttributedString in
                if let pair = x.find(pattern: "｜(.+?)《(.+?)》") {
                    let baseText = (x as NSString).substring(with: pair.range(at: 1))
                    let ruby = (x as NSString).substring(with: pair.range(at: 2))
                    
                    let rubyAttribute: [AnyHashable: Any] = [
                        kCTRubyAnnotationSizeFactorAttributeName: 0.5,
                        kCTForegroundColorAttributeName: textColor
                    ]
                    
                    let annotation = CTRubyAnnotationCreateWithAttributes(.auto, .auto, .before, ruby as CFString, rubyAttribute as CFDictionary)
                    
                    return NSAttributedString(
                        string: baseText,
                        attributes: [.font: font,
                                     .foregroundColor: textColor,
                                     kCTRubyAnnotationAttributeName as NSAttributedString.Key: annotation])
                    
                } else {
                    return NSAttributedString(
                        string: x,
                        attributes: [.font: font,
                                     .foregroundColor: textColor]
                    )
                }
            }
            .reduce(NSMutableAttributedString()) { $0.append($1); return $0 }
    
    return attributed
}
```
処理の流れとしては、
文字列をルビのあるものとそうでないもので分けて、それぞれmap内で`NSAttributedString`とし、
最後に`reduce`で結合しているような流れです。

## CTRubyAnnotation
`CTRubyAnnotation`は`CTRubyAnnotationCreateWithAttributes`で生成しています。
色々な記事を見る限り、以前までは`AttributedString`の`.foregroundColor`に指定されている文字色がルビにも適用されていたようなのですが、
今回実装してみたところ適用されていなかったため、`CTRubyAnnotationCreateWithAttributes`と`kCTForegroundColorAttributeName`を利用してルビに色を適用しています。
Attributeで指定することが分かるまでにそこそこ詰まりました。。

```swift
let rubyAttribute: [AnyHashable: Any] = 
    [
        kCTRubyAnnotationSizeFactorAttributeName: 0.5,
        kCTForegroundColorAttributeName: textColor
    ]
                    
let annotation = CTRubyAnnotationCreateWithAttributes(.auto, 
                                                      .auto, 
                                                      .before, 
                                                      ruby as CFString, 
                                                      rubyAttribute as CFDictionary)
```

それぞれのプロパティ概要は以下の通りです。

```swift
CTRubyAnnotationRef CTRubyAnnotationCreateWithAttributes(
    CTRubyAlignment alignment,// ルビとテキストの長さが違う場合の設定の仕方
    CTRubyOverhang overhang,// ルビがテキストより長い場合に他テキストの上にまで表示されて良いか
    CTRubyPosition position,// テキストに対するルビの位置
    CFStringRef string,// 文字列
    CFDictionaryRef attributes )// 付加情報(サイズや文字色等..)
```

# ルビをUILabelに適用する

ルビの高さの分描画してあげる必要があるので、UILabelを継承したカスタムクラスを作成しました。

```swift:RubyLabel.swift
class RubyLabel: UILabel {

    override func draw(_ rect: CGRect) {
        guard let context = UIGraphicsGetCurrentContext(),
            let attributed = self.attributedText else { return }
        
        context.textMatrix = CGAffineTransform.identity
        context.translateBy(x: 0, y: rect.height)
        context.scaleBy(x: 1.0, y: -1.0)
        
        let frame = CTFramesetterCreateFrame(CTFramesetterCreateWithAttributedString(attributed),
                                             CFRangeMake(0, attributed.length),
                                             CGPath(rect: rect, transform: nil),
                                             nil)
        
        CTFrameDraw(frame, context)

    }
    
    override var intrinsicContentSize: CGSize {
        let baseSize = super.intrinsicContentSize
        
        return CGSize(width: baseSize.width,
                      height: baseSize.height * 1.5)
        
    }

}
```

drawしてあげただけだと下部分が切れてしまうため、
`intrinsicContentSize`をoverrideしてルビの高さ分を加算しています。

<img width="250" alt="スクリーンショット 2018-12-08 18.29.41.png" src="https://qiita-image-store.s3.amazonaws.com/0/110630/e4590697-4894-2d71-145e-8d9440d5ed86.png">

ルビが表示されました！

## ルビのないラベルと高さを合わせる

ルビの分下にずれてしまうのでベースのテキストの位置が合うように`insets`を噛ませました。

```swift:RubyLabel.swift
override func draw(_ rect: CGRect) {
    guard let context = UIGraphicsGetCurrentContext(),
        let attributed = self.attributedText else { return }
    
    let newRect = rect.inset(by: UIEdgeInsets(top: self.font.pointSize / 2, left: 0, bottom: 0, right: 0))

        
    context.textMatrix = CGAffineTransform.identity
    context.translateBy(x: 0, y: newRect.height)
    context.scaleBy(x: 1.0, y: -1.0)
        
    let frame = CTFramesetterCreateFrame(CTFramesetterCreateWithAttributedString(attributed),
                                         CFRangeMake(0, attributed.length),
                                         CGPath(rect: newRect, transform: nil),
                                         nil)
        
    CTFrameDraw(frame, context)

}
    
override var intrinsicContentSize: CGSize {
    let baseSize = super.intrinsicContentSize
        
    return CGSize(width: baseSize.width,
                  height: baseSize.height * 1.5 + self.font.pointSize / 2)
        
}
```

# おわりに

コード全体はこちらになります。
https://github.com/y-negi/RubySample