複数行のテキストを入力表示する

Published by @SoNiceInfo at 6/24/2020


iOS 14対応

WWDC20で発表された新しいSwiftUIからTextEditorが追加されました。
iOS13ではUITextViewをSwiftUIでラップしていましたがTextEditorの登場により不要となりました。
複数のテキストを表示する際にはTextの修飾子.lineLimit(nil)を使って複数行にできます。

import SwiftUI

struct ContentView: View {
    @State var text: String = ""

    var body: some View {
        VStack {
            // 入力
            TextEditor(text: $text)
                .frame(width: UIScreen.main.bounds.width * 0.8, height: 200)
                .overlay(
                    RoundedRectangle(cornerRadius: 10)
                        .stroke(Color.blue, lineWidth: 5)
                )
            // 表示
            Text(text)
                .foregroundColor(.yellow)
                .lineLimit(nil)
                .padding(5)
                .frame(width: UIScreen.main.bounds.width * 0.8, height: 200, alignment: .topLeading)
                .overlay(
                    RoundedRectangle(cornerRadius: 10)
                        .stroke(Color.green, lineWidth: 5)
                )
        }
    }
}

iOS 13対応

複数のテキストを表示する際にはTextの修飾子.lineLimit(nil)を使って複数行にできます。
しかし、入力の際にTextField().lineLimit(nil)としても複数行にはなりません。
そこで、UIViewRepresentableを使ってUIKitのUITextViewをSwiftUIで使えるようにラップします。

複数行に対応したテキストフィールドの構造体を用意する

struct MultilineTextField: UIViewRepresentable {
    @Binding var text: String
}

複数行のテキスト入力に対応したMultilineTextFieldを作ります。
その際、UIViewRepresentableプロトコルに準拠させます。
こうすることでUITextViewを使うことができるようになります。
また、MultilineTextFieldはBindingされた変数textを受け取って参照・変更できるようにします。

UITextViewでテキストフィールドを作る

struct MultilineTextField: UIViewRepresentable {
    @Binding var text: String

    func makeUIView(context: Context) -> UITextView {
        let view = UITextView()
        view.isScrollEnabled = true
        view.isEditable = true
        view.font = UIFont.systemFont(ofSize: 18)
        return view
    }

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

次に、Viewが作られたときに呼ばれるmakeUIViewと更新があったときに呼ばれるupdateUIViewを実装していきます。
makeUIViewUITextViewのインスタンスを作成しています。
view.isScrollEnabled = trueでスクロールの可否、view.isEditable = trueで編集可能なテキストフィールドにしています。

updateUIViewで、変数textが変更されたら、MultilineTextFieldの内容も変更するようにしています。

テキストの変更を親Viewを通知する

struct MultilineTextField: UIViewRepresentable {
    @Binding var text: String

    func makeUIView(context: Context) -> UITextView {
        view.delegate = context.coordinator
        return view
    }

    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }

    class Coordinator : NSObject, UITextViewDelegate {

        var parent: MultilineTextField

        init(_ textView: MultilineTextField) {
            self.parent = textView
        }

        func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
            return true
        }

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

このままではMultilineTextFieldで変更した変数textが親Viewに通知されず使いづらいです。
Coordinatorインスタンスを作成して子Viewから親Viewへ変更通知をします。
makeUIViewでもview.delegate = context.coordinatorを追加します。

完成

今までの説明をまとめるとこのようになります。
SwiftUIの通常のTextField()のようにMultilineTextField(text: $text)で呼び出します。
Viewの修飾子が使えます。今回は青枠が作成したMultilineTextField(text: $text)です。
緑枠に結果が表示されます。

//
//  ContentView.swift
//

import SwiftUI

struct ContentView: View {
    @State var text: String = ""

    var body: some View {
        VStack {
            // 入力
            MultilineTextField(text: $text)
                .frame(width: UIScreen.main.bounds.width * 0.8, height: 200)
                .overlay(
                    RoundedRectangle(cornerRadius: 10)
                        .stroke(Color.blue, lineWidth: 5)
                )
            // 表示
            Text(text)
                .foregroundColor(.yellow)
                .lineLimit(nil)
                .padding(5)
                .frame(width: UIScreen.main.bounds.width * 0.8, height: 200, alignment: .topLeading)
                .overlay(
                    RoundedRectangle(cornerRadius: 10)
                        .stroke(Color.green, lineWidth: 5)
                )
        }
    }
}

// 複数行入力するためのTextField
struct MultilineTextField: UIViewRepresentable {
    @Binding var text: String

    func makeUIView(context: Context) -> UITextView {
        let view = UITextView()
        view.delegate = context.coordinator
        view.isScrollEnabled = true
        view.isEditable = true
        view.isUserInteractionEnabled = true
        view.font = UIFont.systemFont(ofSize: 18)
        return view
    }

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

    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
    
    class Coordinator : NSObject, UITextViewDelegate {

        var parent: MultilineTextField

        init(_ textView: MultilineTextField) {
            self.parent = textView
        }

        func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
            return true
        }

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

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
複数行のテキストを入力表示する画像

キーボードが邪魔で表示が隠れてしまってますね。
こんなときは「キーボードを閉じる」を実装しましょう。

参考

ios - How do I create a multiline TextField in SwiftUI? - Stack Overflow
SwiftUI Multiline Text - Swiftly Dierkes

    アプリをリリースしました!

    ToDoアプリ

    iCloudを利用したデバイス間データ共有、ダークモードに対応しています。

    リリースしたToDoアプリのスクリーンショット

    IPアドレス履歴保存アプリ

    取得したIPアドレスを確認、位置情報とともに保存できます。

    リリースしたIPアドレス保存アプリのスクリーンショット