KINTO Tech Blog
iOS

UITextViewをSwiftUIで使えるようにUIViewRepresentableする

Cover Image for 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() でいい感じに計算すればいいんだ〜というところに辿り着くまでに結構遠回りしたので、記事にしてみました🤓

Facebook

関連記事 | Related Posts

We are hiring!

【iOSエンジニア】モバイルアプリ開発G/大阪

モバイルアプリ開発GについてKINTOテクノロジーズにおける、モバイルアプリ開発のスペシャリストが集まっているグループです。KINTOやmy routeなどのサービスを開発・運用しているグループと協調しながら品質の高いモバイルアプリを開発し、サービスの発展に貢献する事を目標としています。

【iOS/Androidエンジニア】モバイルアプリ開発G/東京・大阪

モバイルアプリ開発GについてKINTOテクノロジーズにおける、モバイルアプリ開発のスペシャリストが集まっているグループです。KINTOやmy routeなどのサービスを開発・運用しているグループと協調しながら品質の高いモバイルアプリを開発し、サービスの発展に貢献する事を目標としています。