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
We are hiring!
【iOSエンジニア】モバイルアプリ開発G/大阪
モバイルアプリ開発GについてKINTOテクノロジーズにおける、モバイルアプリ開発のスペシャリストが集まっているグループです。KINTOやmy routeなどのサービスを開発・運用しているグループと協調しながら品質の高いモバイルアプリを開発し、サービスの発展に貢献する事を目標としています。
【iOS/Androidエンジニア】モバイルアプリ開発G/東京
モバイルアプリ開発GについてKINTOテクノロジーズにおける、モバイルアプリ開発のスペシャリストが集まっているグループです。KINTOやmy routeなどのサービスを開発・運用しているグループと協調しながら品質の高いモバイルアプリを開発し、サービスの発展に貢献する事を目標としています。