SwiftUIでルビをふる
おはようございます。waturaです。新しいmac miniがほしいなぁと思っているんですが,やっぱり、独立した画面ほしいよね。机の上にもう1セットキーボードとかおくのいやだよね。とかって考えると、ほしいのはノートパソコンでは?となっています。
note Mobile Tech Talk #1で発表した内容になります 。
ルビをふりたい
ルビをふりたい noteでルビってどうやってふるんだろう?って検索しないといけないくらい、ルビの使い方がわからなかったんですが、noteでもちゃんとルビはふれるようです。
アプリに入力補助がほしいな!と思いました。が閑話休題。
iOSアプリ上ではルビはふれるのだろうか?
結論:ふれるけど、完璧とはいいがたい。さらに、SwiftUIのみでルビをふる機能は2024年11月時点ではなさそうです。
表題が「SwiftUIでルビをふる」という記事なので、これで終了ですといきたいところですが,SwiftUIからUIKitなどを呼び出せるので,それをつかってルビをふるという方法を説明します。
まず、AttributedStringにはルビAttributeはないようです。NSAttributedStringにもルビAttributeはないようです。CoreTextまでおりてくるとルビAttributeがあります。
CTRubyAnnotationCreateWithAttributes(::::_:)をつかうと
ルビとして表示する文字
表示される位置や表示方法
ベースの文字幅に合わせて表示とか
ルビが長すぎるときの処理方法
ルビの表示位置(上下とか)
などが設定でき、ベースのフォントサイズに対する相対サイズでのサイズ指定や文字色の指定などができます。
このCTRubyAnnotationCreateの戻り値をNSAttributedStringのAttributeに指定してあげると、ルビ表示ができるようになります。
let rubyAttribute: [CFString: Any] = [
kCTRubyAnnotationSizeFactorAttributeName: 0.5
]
let rubyAnnotation = CTRubyAnnotationCreateWithAttributes(
.auto,
.auto,
.before,
rubyText as CFString,
rubyAttribute as CFDictionary
)
NSAttributedString(string: baseText, attributes: [
kCTRubyAnnotationAttributeName as NSAttributedString.Key: rubyAnnotation
])
簡単!これでUITextViewとかでルビが表示できます!
これを、AttributedStringに変換してTextに渡してあげれば、SwiftUIでもルビが!って少し期待したのですが、だめでした。
AttributedStringにした時点で、AttributeからkCTRubyAnnotationAttributeNameが消失してしまうようでした。
SwiftUIアプリでルビを表示する
ざっくり、以下の4つの方法を試してみました。
SwiftUIは諦めてUIViewControllerでUITextView/UILabelを使う
UITextViewをUIViewRepresentableでくるんでSwiftUIから呼び出す
Canvasを使う
TextRendererを使う
残念なことにどの方法もちゃんとSwiftUIだよ!といいきれる実装ではなく、実際に実装している内容はUIKitだったりCoreTextだったりします。
残念です。
表示する文章
だれもが創作をはじめ、
続けられるようにする。
本来であれば、このnoteルビ記法をパースして表示できるようにしたほうがよかったのですが、簡単のためはぶいています。
func rubyAnnotation(text: String, ruby: String) -> NSAttributedString {
let rubyAttribute: [CFString: Any] = [
kCTRubyAnnotationSizeFactorAttributeName: 0.5
]
let rubyAnnotation = CTRubyAnnotationCreateWithAttributes(
.auto,
.auto,
.before,
ruby as CFString,
rubyAttribute as CFDictionary
)
return NSAttributedString(
string: text,
attributes:
[
kCTRubyAnnotationAttributeName as NSAttributedString.Key: rubyAnnotation
]
)
}
var dummyAttributedString: NSAttributedString {
let txt = NSMutableAttributedString(string: "だれもが")
txt.append(rubyAnnotation(text: "創作", ruby: "そうさく"))
txt.append(.init(string: "をはじめ、\n"))
txt.append(rubyAnnotation(text: "続", ruby: "つづ"))
txt.append(.init(string: "けられるようにする。"))
// ルビが範囲外に表示されてしまうので行間を広げる
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.lineHeightMultiple = 1.5
txt.addAttributes([
.font: UIFont.systemFont(ofSize: 16),
.paragraphStyle: paragraphStyle,
], range: .init(location: 0, length: txt.length))
return txt
}
1. SwiftUIは諦めてUIViewControllerでUITextView/UILabelを使う
SwiftUIとUIKitは共存できます。なので、UIViewControllerで表示するというのも1手です。表示は以下のコードをAutoLayoutごにょごにょしたりして表示するだけです。
しかし、残念なことにただそのまま表示するだけではだめでした。
let view = UITextView()
view.attributedText = dummyAttributedString
1行目はともかく2行目が1行目とかぶってしまい読めなくなっています。
また、背景色を指定するとわかりやすいのですが1行目のふりがなも範囲外になってしまっています。
なので、NSAttributedStringを作るときに、lineHeightMultipleを指定するといい感じになります。
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.lineHeightMultiple = 1.5
1行しかない場合は、TextView自体のcontentInsetやtextContainerInsetなどをいい感じに設定してあげると表示できます。こちらの指定だけだと、2行目以降でかぶってしまうので、ある程度以上のlineHeightMultipleが設定されている前提でのデザイン組がいいと思われます。
2. UITextViewをUIViewRepresentableでくるんでSwiftUIから呼び出す
基本的にはUIViewControllerから呼び出すのと変わりません。
struct RubyTextView: UIViewRepresentable {
func makeUIView(context _: Context) -> UITextView {
let view = UITextView()
view.attributedText = dummyAttributedString
return view
}
func updateUIView(_: UITextView, context _: Context) {}
}
これをSwiftUIのScrollViewにいれるなどしたりするとおもいます。そのさいに表示される領域のサイズを考えたりとかっていうのが必要になってくるかとおもいます。
3. Canvas
CanvasはSwiftUIでGraphicsContextを扱うためのViewです。GraphicsContextは2Dのお絵書きをするためのstructです。GraphicsContextはCoreTextをつかった描画ができます。
すなわち、kCTRubyAnnotationAttributeNameがちゃんと仕事をするのです!また、Textをつかった描画もできます。
注意点としては、
Core Graphicsの座標系(左上が原点)をCore Textの座標系(左下が原点)なので、座標を変換する必要があります。
context.scaleBy(x: 1, y: -1)
context.translateBy(x: 0, y: -size.height)
また、Canvasをスクロールする場合にはScrollViewでframeをいい感じにする必要があったりするので,めっちゃ楽っす!!!っていうかんじまではいきません。
kCTRubyAnnotationがCoreTextの機能であるため,1や2のようにLineHeightを調製しないと表示がくずれるとかはありません。
4. TextRenderer
TextRendererはiOS 18から使えるようになったTextRendererというものがあります。Textのレンダリング時に介入できる機能になります。
レンダリングに介入できる→本来表示する文字の上にルビもついでにレンダリングしてあげたらいいんじゃない???という考えでやってみました。
TextRendererは別記事を書いています。アドベントカレンダーとかそういう系で出したいなぁって思っています。
その他思いついた方法
UIViewControllerの中でCoreTextを使う
気合いのText地獄
一つ目はSwiftUIを信じていくとい点で、SwiftUIでも同じようなことができるしまあ、いいかっていうかんじでやっていません。
気合いのText地獄
LazyVStack {
HStack(alignment: .bottom, spacing: 0) {
Text("だれもが")
VStack(spacing: -2) {
Text("そうさく")
.font(.caption2)
Text("創作")
}
Text("をはじめ、")
}
HStack(alignment: .bottom, spacing: 0) {
VStack(spacing: -2) {
Text("つづ")
.font(.caption2)
Text("続")
}
Text("けられるようにする。")
}
}
このコードをClaudeになげていろいろ相談していたら、CoreTextもUIKitもつかわないで、それっぽく動くものが出来てしまいました。
↑の雑Textの組みあわせたくらいで、ほとんどClaudeがつくって私はコピペ・デバッグ係に徹した出力結果です。表示している文言もClaudeがつくったよ!普通に嘘かいてあるよ!!!
まとめ
UIViewController, UIViewRepresentable
SwiftUIじゃないのがいやだよね
LineHeightとかの調整が必要
SwiftUIアプリだったら、SwiftUI向けにつくった資産が使えない
UITextView/UILabelだからみんないろいろ知見もってるよね!
UIKit画面としてつくるなら一番いいのでは?
ScrollViewに埋め込むときの対応
Canvas
CoreGraphics由来なので高度な描画機能が使える
CoreGraphics由来なのでルビ表示がきれいにできる
座標系がかわってしまうので、ややこしい時がある
ScrollViewに埋め込むときの対応
CoreGraphicsようわからん問題
TextRenderer
他にも面白いことができそう
下書きにはTextRendererの記事が控えている
新しく作るならば,SwiftUIをメインでやっていきたいなぁと思っているのでUIViewControllerやUIViewRepresentableではなくCanvasかClaude先生が生み出したものをより深掘っていこうかと思っています。
ソースコード
最後のClaude先生に作ってもらったもの以外のサンプルコードです。