SwiftUIでルビをふる
おはようございます。waturaです。新しいmac miniがほしいなぁと思っているんですが,やっぱり、独立した画面ほしいよね。机の上にもう1セットキーボードとかおくのいやだよね。とかって考えると、ほしいのはノートパソコンでは?となっています。
note Mobile Tech Talk #1で発表した内容になります。
note Mobile Tech Talk #1 (2024/11/20 12:00〜) # 📝概要 note株式会社 AppチームによるモバイルアプリエンジニアLTイベントです。 noteのiOS / An pieceofcake.connpass.com
ルビをふりたい
ルビをふりたい noteでルビってどうやってふるんだろう?って検索しないといけないくらい、ルビの使い方がわからなかったんですが、noteでもちゃんとルビはふれるようです。
ルビ(ふりがな)をふる
アプリに入力補助がほしいな!と思いました。が閑話休題。
iOSアプリ上ではルビはふれるのだろうか?
**結論:ふれるけど、完璧とはいいがたい。**さらに、SwiftUIのみでルビをふる機能は2024年11月時点ではなさそうです。表題が「SwiftUIでルビをふる」という記事なので、これで終了ですといきたいところですが,SwiftUIからUIKitなどを呼び出せるので,それをつかってルビをふるという方法を説明します。
まず、AttributedStringにはルビAttributeはないようです。NSAttributedStringにもルビAttributeはないようです。CoreTextまでおりてくるとルビAttributeがあります。
CTRubyAnnotationCreateWithAttributes(_:_:_:_:_:) | Apple Developer Documentation Creates an immutable ruby annotation object with the specifie developer.apple.com
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が設定されている前提でのデザイン組がいいと思われます。
lineHeightMultipleを1.6に指定
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をつかった描画ができます。
Canvas | Apple Developer Documentation A view type that supports immediate mode drawing. developer.apple.com
GraphicsContext | Apple Developer Documentation An immediate mode drawing destination, and its current state. developer.apple.com
すなわち、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を調製しないと表示がくずれるとかはありません。
CanvasだとLineHeightの調整がいらない
4. TextRenderer
TextRendererはiOS 18から使えるようになったTextRendererというものがあります。Textのレンダリング時に介入できる機能になります。
TextRenderer | Apple Developer Documentation A value that can replace the default text view rendering beha developer.apple.com
レンダリングに介入できる→本来表示する文字の上にルビもついでにレンダリングしてあげたらいいんじゃない???という考えでやってみました。
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がつくったよ!普通に嘘かいてあるよ!!!
Claudeさんがつくったnote活用ガイド
まとめ
UIViewController, UIViewRepresentable
SwiftUIじゃないのがいやだよね
LineHeightとかの調整が必要
SwiftUIアプリだったら、SwiftUI向けにつくった資産が使えない
UITextView/UILabelだからみんないろいろ知見もってるよね!
UIKit画面としてつくるなら一番いいのでは?
ScrollViewに埋め込むときの対応
Canvas
CoreGraphics由来なので高度な描画機能が使える
CoreGraphics由来なのでルビ表示がきれいにできる
座標系がかわってしまうので、ややこしい時がある
ScrollViewに埋め込むときの対応
CoreGraphicsようわからん問題
TextRenderer
他にも面白いことができそう
下書きにはTextRendererの記事が控えている
新しく作るならば,SwiftUIをメインでやっていきたいなぁと思っているのでUIViewControllerやUIViewRepresentableではなくCanvasかClaude先生が生み出したものをより深掘っていこうかと思っています。
ソースコード
最後のClaude先生に作ってもらったもの以外のサンプルコードです。
Discussion in the ATmosphere