More than 1 year has passed since last update.

 やっはろー。iOS 8 より Core Text に CTRubyAnnotationRef が追加され、文字列にルビを振れるようになりました、という話をします。

 Core Text の CTRubyAnnotationCreate を使って CTRubyAnnotationRef をつくります。アライメント・サイズ・位置の設定ができますが、普通に使う分には自動で良いと思います。

var text = [.passRetained(ruby) as Unmanaged<CFStringRef>?, .None, .None, .None]
let annotation = CTRubyAnnotationCreate(.Auto, .Auto, 0.5, &text)

あとは、kCTRubyAnnotationAttributeName とペアで NSAttributeString に渡し、Core Graphic で描画します。

import UIKit

class View: UIView {
    override func drawRect(rect: CGRect) {
        let string = "桐間紗路"
        let ruby = "シャロ"

        var text = [.passRetained(ruby) as Unmanaged<CFStringRef>?, .None, .None, .None]
        let annotation = CTRubyAnnotationCreate(.Auto, .Auto, 0.5, &text)

        let attributed = NSAttributedString(string: string, attributes: [
            NSFontAttributeName: UIFont(name: "HiraMinProN-W6", size: 50.0)!,
            NSForegroundColorAttributeName: UIColor(red: 0.788, green: 0.522, blue: 0.337, alpha: 1.0),
            kCTRubyAnnotationAttributeName as String: annotation,
        ])

        let size = attributed.boundingRectWithSize(
            CGSizeMake(rect.width, rect.width),
            options: .UsesLineFragmentOrigin,
            context: nil)

        let context = UIGraphicsGetCurrentContext()

        CGContextSetRGBFillColor(context, 0.984, 0.922, 0.69, 1.0)
        CGContextAddRect(context, rect)
        CGContextFillPath(context)

        CGContextTranslateCTM(context, (rect.width - size.width) / 2.0, 200.0)
        CGContextScaleCTM(context, 1.0, -1.0)

        let line = CTLineCreateWithAttributedString(attributed)
        CTLineDraw(line, context!)
    }
}

class ViewController: UIViewController {
    override func loadView() {
        super.loadView()

        self.view = View()
    }
}

カフェインハイテンション

楽しい!₍₍ (ง╹◡╹)ว ⁾⁾

 尺が余ったので名詠式やります。ルビの振りは、青空文庫における表記を真似て|文章《読み》としています。

import UIKit

extension String {
    func find(pattern pattern: String) -> NSTextCheckingResult? {
        do {
            let re = try NSRegularExpression(pattern: pattern, options: [])
            return re.firstMatchInString(
                self,
                options: [],
                range: NSMakeRange(0, self.utf16.count))
        } catch {
            return nil
        }
    }

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

class View: UIView {
    override func drawRect(rect: CGRect) {
        let text = [
            "「まさか、|後罪《クライム》の|触媒《カタリスト》を〈|讃来歌《オラトリオ》〉無しで?」",
            "教師たちの狼狽した声が次々と上がる。",
            "……なんでだろう。何を驚いているんだろう。",
            "ただ普通に、この|触媒《カタリスト》を使って|名詠門《チャネル》を開かせただけなのに。",
            "そう言えば、何を|詠《よ》ぼう。",
            "自分の一番好きな花でいいかな。",
            "どんな宝石より素敵な、わたしの大好きな緋色の花。",
            "――『|Keinez《赤》』――",
            "そして、少女の口ずさんだその後に――",
        ]
        .joinWithSeparator("\n")

        let attributed =
            text
            .replace(pattern: "(|.+?《.+?》)", template: ",$1,")
            .componentsSeparatedByString(",")
            .map { x -> NSAttributedString in
                if let pair = x.find(pattern: "|(.+?)《(.+?)》") {
                    let string = (x as NSString).substringWithRange(pair.rangeAtIndex(1))
                    let ruby = (x as NSString).substringWithRange(pair.rangeAtIndex(2))

                    var text = [.passRetained(ruby) as Unmanaged<CFStringRef>?, .None, .None, .None]
                    let annotation = CTRubyAnnotationCreate(.Auto, .Auto, 0.5, &text)

                    return NSAttributedString(
                        string: string,
                        attributes: [kCTRubyAnnotationAttributeName as String: annotation])
                } else {
                    return NSAttributedString(string: x, attributes: nil)
                }
            }
            .reduce(NSMutableAttributedString()) { $0.appendAttributedString($1); return $0 }

        var height = 28.0
        let settings = [
            CTParagraphStyleSetting(
                spec: .MinimumLineHeight,
                valueSize: Int(sizeofValue(height)),
                value: &height)
        ]
        let style = CTParagraphStyleCreate(settings, Int(settings.count))

        attributed.addAttributes([
                NSFontAttributeName: UIFont(name: "HiraMinProN-W3", size: 14.0)!,
                NSVerticalGlyphFormAttributeName: true,
                kCTParagraphStyleAttributeName as String: style,
            ],
            range: NSMakeRange(0, attributed.length))

        let context = UIGraphicsGetCurrentContext()

        CGContextSetRGBFillColor(context, 1.0, 1.0, 1.0, 1.0)
        CGContextAddRect(context, rect)
        CGContextFillPath(context)

        CGContextRotateCTM(context, CGFloat(M_PI_2))
        CGContextTranslateCTM(context, 30.0, 35.0)
        CGContextScaleCTM(context, 1.0, -1.0)

        let framesetter = CTFramesetterCreateWithAttributedString(attributed)
        let path = CGPathCreateWithRect(CGRectMake(0.0, 0.0, rect.height, rect.width), nil)
        let frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, 0), path, nil)
        CTFrameDraw(frame, context!)
    }
}

class ViewController: UIViewController {
    override func loadView() {
        super.loadView()

        self.view = View()
    }
}

まさか、後罪の触媒を〈讃来歌〉無しで?

楽しい!₍₍ (ง╹◡╹)ว ⁾⁾