SwiftUIで縦書きを実現するTategakiText
動作イメージ
- textを渡すと縦書き表示されます。
- カギかっこや、ビックリやハテナ記号、3点リーダーなども縦書きに合わせた表示になっています。
ソースコード
SwiftUIで電子書籍っぽい動作
swiftuiに移植終わった
— ふじき (@fzkqi) April 18, 2020
とてもいい感じ pic.twitter.com/jILnpdbHYp
TategakiText
TategakiText
- TategakiTextViewのswiftuiのwrapper
- UIViewRepresentableを使って、UIViewのサブクラスをswiftuiで使っている
public struct TategakiText: UIViewRepresentable {
public var text: String?
public func makeUIView(context: Context) -> TategakiTextView {
let uiView = TategakiTextView()
uiView.isOpaque = false
uiView.text = text
return uiView
}
public func updateUIView(_ uiView: TategakiTextView, context: Context) {
uiView.text = text
}
}
TategakiTextView
- UIViewのサブクラス。
- CoreTextを使って縦書きでテキストを表示する。
CoreTextでテキストを表示する
- CTFrameが上下反転表示されるので、CGContextで上下反転させて表示する。
public class TategakiTextView: UIView {
override public func draw(_ rect: CGRect) {
guard let context:CGContext = UIGraphicsGetCurrentContext() else {
return
}
context.scaleBy(x: 1, y: -1)
context.translateBy(x: 0, y: -rect.height)
ctFrame = textからCTFrameを作成する諸々
CTFrameDraw(ctFrame, context)
}
}
textからCTFrameを作成する諸々
- verticalGlyphFormを有効にしたCTFramesetterを作成する。
let baseAttributes: [NSAttributedString.Key : Any] = [
.verticalGlyphForm: true,
]
let attributedText = NSMutableAttributedString(string: text ?? "", attributes: baseAttributes)
let setter = CTFramesetterCreateWithAttributedString(attributedText)
- CTFrameProgression.rightToLeftの形式でCTFrameを作成する
let path = CGPath(rect: rect, transform: nil)
let frameAttrs = [
kCTFrameProgressionAttributeName: CTFrameProgression.rightToLeft.rawValue,
]
ctFrame = CTFramesetterCreateFrame(setter, CFRangeMake(0, 0), path, frameAttrs as CFDictionary)
全文
import UIKit
import CoreText
import SwiftUI
struct TategakiText_Previews: PreviewProvider {
static var previews: some View {
TategakiText(text: """
こんにちは、
わたしの名前はふじきです。
これはswiftuiで実装されています。
「どうやって実装してるのか」って?
それはQiitaの記事を読むとわかりますよ!
""").padding(40)
}
}
public struct TategakiText: UIViewRepresentable {
public var text: String?
public func makeUIView(context: Context) -> TategakiTextView {
let uiView = TategakiTextView()
uiView.isOpaque = false
uiView.text = text
return uiView
}
public func updateUIView(_ uiView: TategakiTextView, context: Context) {
uiView.text = text
}
}
public class TategakiTextView: UIView {
public var text: String? = nil {
didSet {
ctFrame = nil
}
}
private var ctFrame: CTFrame? = nil
override public func draw(_ rect: CGRect) {
guard let context:CGContext = UIGraphicsGetCurrentContext() else {
return
}
context.scaleBy(x: 1, y: -1)
context.translateBy(x: 0, y: -rect.height)
let baseAttributes: [NSAttributedString.Key : Any] = [
.verticalGlyphForm: true,
NSAttributedString.Key.font: UIFont.hiraMinProN_W6(size: 25),
]
let attributedText = NSMutableAttributedString(string: text ?? "", attributes: baseAttributes)
let setter = CTFramesetterCreateWithAttributedString(attributedText)
let path = CGPath(rect: rect, transform: nil)
let frameAttrs = [
kCTFrameProgressionAttributeName: CTFrameProgression.rightToLeft.rawValue,
]
let ct = CTFramesetterCreateFrame(setter, CFRangeMake(0, 0), path, frameAttrs as CFDictionary)
ctFrame = ct
CTFrameDraw(ct, context)
}
}
終わりに
SwiftUIは、SwiftUIを書いていたはずが、気付いたらUIKitを書いていて、複雑な気持ちになりがちです。