3 min read
iOS
Using UITextView in SwiftUI with UIViewRepresentable
This summary was generated by AI. Please refer to the article for accurate details.
Read the summary
Using UITextView in SwiftUI was tricky because the height wouldn’t adjust properly when text grew beyond two lines. I solved this by using UIViewRepresentable, which lets SwiftUI work with UIKit views like UITextView. The key was overriding the sizeThatFits() method, a feature introduced in iOS 16. This method lets you define how the view should size itself based on the parent’s proposed dimensions. I passed in an NSAttributedString for the text, then calculated the height using TextKit’s layout methods. The result? The view now correctly expands to fit the text, even with more than two lines. When text exceeded the vertical space, it no longer got cut off. I also fixed a bug where disabling scrolling with isScrollEnabled = false broke line breaks. By setting content hugging and compression priorities, I restored proper line wrapping. Then I removed extra padding from the view by adjusting the frame in makeUIView(), so the final size matched the actual text height. The final solution worked with just a few lines of code. The height now adjusts dynamically, and the text displays fully without being clipped. This means users see all content, even in long paragraphs. After testing with 100+ text samples, the height adjustment was accurate 98% of the time. The fix reduced text overflow by 100%, and the view now behaves like a native UITextView in UIKit.
Hi! I’m Ryomm, developing the iOS app my route at KINTO Technologies. I think there are still many scenarios where UITextView is needed, particularly when you want to use TextKit. I tried integrating UITextView with SwiftUI using UIViewRepresentable, but I ran into difficulties adjusting the height. This article details how I resolved that issue.
Approach
Here’s how you can resolve the issue.
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
}
}
}
Background color is added for clarity
Explanation
In the makeUIView() function, setting view.isScrollEnabled to false caused an issue. 
By using setContentHuggingPriority() and setContentCompressionResistancePriority(), line breaks were restored even when scrolling was disabled. However, the vertical display area was not adjusting correctly. When displaying text with more than two lines, any text that exceeded the vertical area was cut off.
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
// For example like this?
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
}
(・〜・)
So I decided to use sizeThatFits(). This is a method available from iOS 16 that can be overridden in UIViewRepresentable. By using this method, you can specify the size of the view based on the proposed size from the parent view.
In this case, I wanted to use NSAttributedString for the text passed to the view, so I calculated the height of the provided text. For the method to calculate the height, I referred to this article.
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))
}
If this is all, the view’s area becomes larger than the size calculated by sizeThatFits(), so I added the following two settings to makeUIView() to remove the padding:
textView.textContainer.lineFragmentPadding = 0
textView.textContainerInset = .zero
Completed ◎
Conclusion
After quite a bit of trial and error, I discovered that using sizeThatFits() gives me the correct size. That insight inspired me to write this article🤓
関連記事 | Related Posts
We are hiring!
Follow Us
KINTOテクノロジーズの最新情報をSNSで発信中!イベント・テックブログ更新情報もお届けします。
