UITextViewをSwiftUIで使えるようにUIViewRepresentableする
KINTOテクノロジーズで my route(iOS) を開発しているRyommです。
TextKitを使いたい場合など、未だUITextViewは必要になることが多いと思います。
UITextViewをSwiftUIで使えるようにUIViewRepresentableしようとしたところ、高さの調整にハマったので、その解決記事です。
結論
こんな感じでできます。
import UIKit
struct TextView: UIViewRepresentable {
var text: NSAttributedString
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIView(context: Context) -> UITextView {
let view = UITextView()
view.delegate = context.coordinator
view.isScrollEnabled = false
view.isEditable = false
view.isUserInteractionEnabled = false
view.isSelectable = false
view.backgroundColor = .clear
view.textContainer.lineFragmentPadding = 0
view.textContainerInset = .zero
return view
}
func updateUIView(_ uiView: UITextView, context: Context) {
uiView.attributedText = text
}
func sizeThatFits(_ proposal: ProposedViewSize, uiView: UITextView, context: Context) -> CGSize? {
guard let width = proposal.width else { return nil }
let dimensions = text.boundingRect(
with: CGSize(width: width, height: CGFloat.greatestFiniteMagnitude),
options: [.usesLineFragmentOrigin, .usesFontLeading],
context: nil)
return .init(width: width, height: ceil(dimensions.height))
}
}
extension TextView {
final class Coordinator: NSObject, UITextViewDelegate {
private var textView: TextView
init(_ textView: TextView) {
self.textView = textView
super.init()
}
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
return true
}
func textViewDidChange(_ textView: UITextView) {
self.textView.text = textView.attributedText
}
}
}

わかりやすさのために背景色をつけてます
解説
makeUIView() において、 view.isScrollEnabled を false にすると、改行がされなくなってしまう問題がありました。

setContentHuggingPriority() や setContentCompressionResistancePriority() を使うと、スクロール無効時も改行はされるようになりましたが、垂直方向の表示領域がうまく調整されません。2行以上のテキストを表示する場合、垂直方向の領域を超えた部分は消えてしまっていました。
func makeUIView(context: Context) -> UITextView {
let view = UITextView()
view.delegate = context.coordinator
view.isScrollEnabled = false
view.isEditable = false
view.isUserInteractionEnabled = false
view.isSelectable = true
view.backgroundColor = .clear
// こんな感じ?
view.setContentHuggingPriority(.defaultHigh, for: .vertical)
view.setContentHuggingPriority(.defaultHigh, for: .horizontal)
view.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
view.setContentCompressionResistancePriority(.required, for: .vertical)
view.textContainer.lineFragmentPadding = 0
view.textContainerInset = .zero
return view
}

(・〜・)
そこで sizeThatFits() を使います。
これはiOS16から提供された、UIViewRepresentableでオーバーライドできるメソッドです。このメソッドを使うと、提案された親のサイズを使ってViewのサイズを指定することができます。
今回はViewに渡すテキストを NSAttributedString にしたかったため、受け取ったテキストの高さを計算します。
高さの計算方法は こちらの記事 を参考にしました。
func sizeThatFits(_ proposal: ProposedViewSize, uiView: UITextView, context: Context) -> CGSize? {
guard let width = proposal.width else { return nil }
let dimensions = text.boundingRect(
with: CGSize(width: width, height: CGFloat.greatestFiniteMagnitude),
options: [.usesLineFragmentOrigin, .usesFontLeading],
context: nil)
return .init(width: width, height: ceil(dimensions.height))
}
これだけだとViewの領域が sizeThatFits() で計算した大きさより大きくなってしまうため、 makeUIView() に以下の2つの設定を入れて余白を消します。
textView.textContainer.lineFragmentPadding = 0
textView.textContainerInset = .zero

できました◎
おわりに
sizeThatFits() でいい感じに計算すればいいんだ〜というところに辿り着くまでに結構遠回りしたので、記事にしてみました🤓
関連記事 | Related Posts
UITextViewをSwiftUIで使えるようにUIViewRepresentableする
[SwiftUI] Define Your Own Style and Write Stylish Code

Swift Observation: A Cleaner and More Efficient Approach to Updating the UI
Snapshot Testing in the my route App
【SwiftUI】独自のStyleを定義してスタイリッシュなコードを書こう

Compose MultiplatformとSwiftUIで作るハイブリッドモバイルアプリ(Part 3):SwiftUI連携と技術Tips
We are hiring!
生成AIエンジニア/AIファーストG/東京・名古屋・大阪・福岡
AIファーストGについて生成AIの活用を通じて、KINTO及びKINTOテクノロジーズへ事業貢献することをミッションに2024年1月に新設されたプロジェクトチームです。生成AI技術は生まれて日が浅く、その技術を業務活用する仕事には定説がありません。
【オープンポジション】「気になる!」方はまずはこちらからご応募ください。/東京・名古屋・大阪・福岡
業務内容国内外のKINTOサービスや、トヨタグループの金融、モビリティサービスの内製開発組織である同社にて、ご経験・ご志向性に応じて配属を決定し、ご活躍いただきます。



