⌨️

SwiftUIで「全高さを使うTextEditor」を実現する

に公開

SwiftUIで「全高さを使うTextEditor」を実現する

SwiftUIで、文字入力を受け付ける TextEditor を「最大高さ」で伸ばし、スクロール可能にする方法を整理します!

まず、FlutterやDartとは違って、SwiftUIのTextEditorは「自然に高さを調整する」ようにはできません。

そこで、自分で調整する必要があります(UIKitのUITextViewを使うとより簡単になりますが)。


SwiftUIのTextEditorの特性と限界

  • SwiftUI標準のTextEditormulti-line入力に特化したコンポーネントです。
  • しかし、文字量によって自動で高さを変更する機能はないため、基本的に高さを固定するか、スクロールするしかありません。
  • 高さを柔軟に変える場合、内部的にUIKitのUITextViewを使う必要が出てきます。

TextEditor自体は、SwiftUIのViewサイクルにうまく乗っていますが、コンテンツサイズの検知・動的なframe更新といった機能までは標準で備えていないのです。


どうやって動的な高さを実現するか(原理解説)

SwiftUIにはUIKitのようなsizeToFitcontentSizeという考え方がありません。そこで:

  1. UIKitのUITextViewUIViewRepresentableでラップしてSwiftUIに持ち込む
  2. UITextViewcontentSizeまたはsizeThatFitsを使い、必要な高さを計算する
  3. その計算結果をSwiftUI側のStateに反映させて、Viewの高さを更新する

これにより、入力内容に応じて伸び縮みするエディタが作れます。

もし "全高を使いたい" 場合は、高さの計算すらせず、常に.frame(maxHeight: .infinity)を指定して、TextEditorが親の最大サイズに広がるようにします。

ここではこの"全高使い切り型"を紹介します。


実装サンプル

FullSizeTextEditor.swift

import SwiftUI

struct FullSizeTextEditor: UIViewRepresentable {
    @Binding var text: String

    func makeUIView(context: Context) -> UITextView {
        let textView = UITextView()
        textView.isScrollEnabled = true // コンテンツが増えたらスクロール
        textView.font = UIFont.preferredFont(forTextStyle: .body)
        textView.backgroundColor = .clear
        textView.delegate = context.coordinator
        textView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
        return textView
    }

    func updateUIView(_ uiView: UITextView, context: Context) {
        if uiView.text != text {
            uiView.text = text
        }
    }

    func makeCoordinator() -> Coordinator {
        Coordinator(text: $text)
    }

    class Coordinator: NSObject, UITextViewDelegate {
        var text: Binding<String>

        init(text: Binding<String>) {
            self.text = text
        }

        func textViewDidChange(_ textView: UITextView) {
            text.wrappedValue = textView.text
        }
    }
}

使い方:TextEditorView.swift

struct TextEditorView: View {
    @State private var text: String = ""

    var body: some View {
        VStack(spacing: 0) {
            FullSizeTextEditor(text: $text)
                .frame(maxWidth: .infinity, maxHeight: .infinity)
                .padding()
                .background(Color(.systemGray6))
                .cornerRadius(8)
                .padding()
        }
    }
}

なぜこうする必要があるのか(もう少し深掘り)

  • SwiftUIのTextEditor単体では"文字量に応じたサイズ調整"ができないため
  • UITextViewcontentSizeを利用すると、内容に応じた自然なレイアウトが可能になる
  • 全高を使う場合は、最大サイズを指定しつつ、スクロール許可することで、オーバーフローを防ぐ
  • これにより、自然な入力体験と、レイアウトの安定性が両立できる

UIKitの知識をうまくSwiftUIに橋渡しするのがポイントです。


プラスアップ(さらに加えられると良いテクニック)

プレースホルダー表示

TextEditorにはデフォルトのプレースホルダーがないため、オーバーレイで実装します。

struct PlaceholderTextEditor: View {
    @Binding var text: String
    var placeholder: String

    var body: some View {
        ZStack(alignment: .topLeading) {
            if text.isEmpty {
                Text(placeholder)
                    .foregroundColor(.gray)
                    .padding(EdgeInsets(top: 8, leading: 4, bottom: 0, trailing: 0))
            }
            FullSizeTextEditor(text: $text)
                .frame(maxWidth: .infinity, maxHeight: .infinity)
        }
    }
}

マックス高さ制限(例:400ptを超えたらスクロール)

最大サイズを制限し、それ以上はスクロールさせる設計も可能です。

FullSizeTextEditor(text: $text)
    .frame(maxWidth: .infinity)
    .frame(minHeight: 100, maxHeight: 400) // 高さ制限
    .background(Color(.systemGray6))
    .cornerRadius(8)

キーボードに押し上げられないようにする

キーボードが出ても固定したい要素(例:送信ボタン)には、.ignoresSafeArea(.keyboard)を指定します。

Button("送信") {
    // 送信処理
}
.frame(maxWidth: .infinity)
.padding()
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(10)
.ignoresSafeArea(.keyboard)

これらの工夫を組み合わせることで、よりプロダクト品質の高い入力体験が作れます。


おわりに

SwiftUIで「高さを入力に対応して変える」のは直接的にはサポートされていませんが、UIKitを組み合わせることで柔軟に実現できます。

実際のプロダクトでもこの手法はかなり役立つので、ぜひ覚えておきたいところです。

Discussion