やっはろー。iOS 8から文字列にルビを振ることができるようになりました。Core Text の CTRubyAnnotation (CTRubyAnnotationRef) を使います。
let ruby = "ルビ"
let string = "文字列"
let annotation = CTRubyAnnotationCreateWithAttributes(.auto, .auto, .before, ruby as CFString, [
kCTRubyAnnotationSizeFactorAttributeName: 0.5,
] as CFDictionary)
let annotatedString = NSAttributedString(string: string, attributes: [
kCTRubyAnnotationAttributeName as NSAttributedString.Key: annotation,
])
例です。
import UIKit
import PlaygroundSupport
extension NSAttributedString.Key {
static let rubyAnnotation: NSAttributedString.Key = kCTRubyAnnotationAttributeName as NSAttributedString.Key
}
extension UIColor {
static let foregroundColor: UIColor = UIColor(red: 0.788, green: 0.522, blue: 0.337, alpha: 1.0)
static let backgroundColor: UIColor = UIColor(red: 0.984, green: 0.922, blue: 0.69, alpha: 1.0)
}
class View: UIView {
lazy var attributedString: NSAttributedString = {
let string = "桐間紗路"
let ruby = "シャロ"
let annotation = CTRubyAnnotationCreateWithAttributes(.auto, .auto, .before, ruby as CFString, [
kCTForegroundColorAttributeName: UIColor.foregroundColor,
kCTRubyAnnotationSizeFactorAttributeName: 0.5,
] as CFDictionary)
let annotatedString = NSAttributedString(string: string, attributes: [
.font: UIFont(name: "HiraMinProN-W6", size: 60.0)!,
.foregroundColor: UIColor.foregroundColor,
.rubyAnnotation: annotation,
])
return annotatedString
}()
override func draw(_ rect: CGRect) {
let size = attributedString.boundingRect(with: rect.size,
options: .usesDeviceMetrics,
context: nil)
guard let context = UIGraphicsGetCurrentContext() else {
return
}
context.setFillColor(UIColor.backgroundColor.cgColor)
context.fill(rect)
context.scaleBy(x: 1, y: -1)
context.translateBy(x: (rect.width - size.width) / 2, y: -1 * (rect.height - size.height) / 2)
let line = CTLineCreateWithAttributedString(attributedString)
CTLineDraw(line, context)
}
}
let frame = UIScreen.main.bounds.applying(CGAffineTransform(scaleX: 0.5, y: 0.5))
PlaygroundPage.current.liveView = View(frame: frame)
楽しい!₍₍ (ง╹◡╹)ว ⁾⁾
ということで名詠式をやります。ルビの振りは青空文庫における表記を真似て |文字列《ルビ》
としています。
import UIKit
import PlaygroundSupport
extension NSAttributedString.Key {
static let rubyAnnotation: NSAttributedString.Key = kCTRubyAnnotationAttributeName as NSAttributedString.Key
}
extension NSMutableAttributedString {
func addAttributes(_ attrs: [NSAttributedString.Key: Any] = [:]) {
addAttributes(attrs, range: NSRange(string.startIndex ..< string.endIndex, in: string))
}
}
extension NSRegularExpression {
func matches(in string: String, options: NSRegularExpression.MatchingOptions = []) -> [NSTextCheckingResult] {
return matches(in: string, options: options, range: NSRange(string.startIndex ..< string.endIndex, in: string))
}
}
class View: UIView {
let fontSize: CGFloat = 16
var lineHeight: CGFloat { fontSize * 1.8 }
lazy var attributedString: NSAttributedString = {
let text = """
「まさか、|後罪《クライム》の|触媒《カタリスト》を〈|讃来歌《オラトリオ》〉無しで?」
教師たちの狼狽した声が次々と上がる。
……なんでだろう。何を驚いているんだろう。
ただ普通に、この|触媒《カタリスト》を使って|名詠門《チャネル》を開かせただけなのに。
そう言えば、何を|詠《よ》ぼう。
自分の一番好きな花でいいかな。
どんな宝石より素敵な、わたしの大好きな緋色の花。
――『|Keinez《赤》』――
そして、少女の口ずさんだその後に――
"""
let attributedString = NSMutableAttributedString(string: text)
for result in try! NSRegularExpression(pattern: "|(.+?)《(.+?)》").matches(in: text).reversed() {
if let string = Range(result.range(at: 1), in: text).map({ String(text[$0]) }),
let ruby = Range(result.range(at: 2), in: text).map({ String(text[$0]) }) {
let annotation = CTRubyAnnotationCreateWithAttributes(.auto, .auto, .before, ruby as CFString, [
kCTRubyAnnotationSizeFactorAttributeName: 0.5,
] as CFDictionary)
let annotatedString = NSAttributedString(string: string, attributes: [
.rubyAnnotation: annotation,
])
attributedString.replaceCharacters(in: result.range, with: annotatedString)
}
}
attributedString.addAttributes([
.font: UIFont(name: "HiraMinProN-W3", size: fontSize)!,
.verticalGlyphForm: true,
])
return attributedString
}()
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else {
return
}
context.setFillColor(UIColor.white.cgColor)
context.fill(rect)
context.rotate(by: .pi / 2)
context.scaleBy(x: 1, y: -1)
context.translateBy(x: 0, y: rect.width)
let framesetter = CTFramesetterCreateWithAttributedString(attributedString)
let frame = CTFramesetterCreateFrame(framesetter, CFRange(), CGPath(rect: rect, transform: nil), [
kCTFrameProgressionAttributeName: CTFrameProgression.rightToLeft.rawValue,
] as CFDictionary)
for line in CTFrameGetLines(frame) as! [CTLine] {
context.translateBy(x: 0, y: -1 * lineHeight)
CTLineDraw(line, context)
}
}
}
let frame = UIScreen.main.bounds.applying(CGAffineTransform(scaleX: 0.5, y: 0.5))
PlaygroundPage.current.liveView = View(frame: frame)
CTFrameDraw
ではなく CTLineDraw
なのは、ルビの有無で行の高さが変わってつらい感じになるからです。どうして。
楽しい!₍₍ (ง╹◡╹)ว ⁾⁾