はじめに
NSAttributedString
を利用したルビの実装についてです。
以下の記事を参考にさせていただきました。
https://qiita.com/woxtu/items/284369fd2654edac2248
こちらに則って、青空文庫形式での実装になります。
例:「デーモン|小暮閣下《こぐれかっか》」
ルビ判別のための正規表現
青空文庫形式自体を判別するための正規表現をextension
で実装しました。
ほぼ参考記事のままとなります。
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として実装しています。
こちらもほぼ参考記事のままとなっています。
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で指定することが分かるまでにそこそこ詰まりました。。
let rubyAttribute: [AnyHashable: Any] =
[
kCTRubyAnnotationSizeFactorAttributeName: 0.5,
kCTForegroundColorAttributeName: textColor
]
let annotation = CTRubyAnnotationCreateWithAttributes(.auto,
.auto,
.before,
ruby as CFString,
rubyAttribute as CFDictionary)
それぞれのプロパティ概要は以下の通りです。
CTRubyAnnotationRef CTRubyAnnotationCreateWithAttributes(
CTRubyAlignment alignment,// ルビとテキストの長さが違う場合の設定の仕方
CTRubyOverhang overhang,// ルビがテキストより長い場合に他テキストの上にまで表示されて良いか
CTRubyPosition position,// テキストに対するルビの位置
CFStringRef string,// 文字列
CFDictionaryRef attributes )// 付加情報(サイズや文字色等..)
ルビをUILabelに適用する
ルビの高さの分描画してあげる必要があるので、UILabelを継承したカスタムクラスを作成しました。
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してルビの高さ分を加算しています。
ルビが表示されました!
ルビのないラベルと高さを合わせる
ルビの分下にずれてしまうのでベースのテキストの位置が合うようにinsets
を噛ませました。
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