SwiftUIで複数行のTextFieldを作成するにはどうすればよいですか?


93

SwiftUIで複数行のTextFieldを作成しようとしていますが、その方法がわかりません。

これは私が現在持っているコードです:

struct EditorTextView : View {
    @Binding var text: String

    var body: some View {
        TextField($text)
            .lineLimit(4)
            .multilineTextAlignment(.leading)
            .frame(minWidth: 100, maxWidth: 200, minHeight: 100, maxHeight: .infinity, alignment: .topLeading)
    }
}

#if DEBUG
let sampleText = """
Very long line 1
Very long line 2
Very long line 3
Very long line 4
"""

struct EditorTextView_Previews : PreviewProvider {
    static var previews: some View {
        EditorTextView(text: .constant(sampleText))
            .previewLayout(.fixed(width: 200, height: 200))
    }
}
#endif

しかし、これは出力です:

ここに画像の説明を入力してください


1
lineLimit()を使用して、GMであるXcodeバージョン11.0(11A419c)でswiftuiを使用して複数行のテキストフィールドを作成しようとしました。それでも機能しません。Appleがこれをまだ修正していないなんて信じられない。複数行のテキストフィールドは、モバイルアプリではかなり一般的です。
e9 8719

回答:


49

更新:Xcode11ベータ4は現在サポートしていますが、編集可能な複数行のテキストを機能さTextViewせるにUITextViewは、aを折り返すことが依然として最良の方法であることがわかりました。たとえばTextView、ビュー内にテキストが正しく表示されない表示の不具合があります。

元の(ベータ1)回答:

今のところ、aUITextViewをラップして構成可能なものを作成できますView

import SwiftUI
import Combine

final class UserData: BindableObject  {
    let didChange = PassthroughSubject<UserData, Never>()

    var text = "" {
        didSet {
            didChange.send(self)
        }
    }

    init(text: String) {
        self.text = text
    }
}

struct MultilineTextView: UIViewRepresentable {
    @Binding var text: String

    func makeUIView(context: Context) -> UITextView {
        let view = UITextView()
        view.isScrollEnabled = true
        view.isEditable = true
        view.isUserInteractionEnabled = true
        return view
    }

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

struct ContentView : View {
    @State private var selection = 0
    @EnvironmentObject var userData: UserData

    var body: some View {
        TabbedView(selection: $selection){
            MultilineTextView(text: $userData.text)
                .tabItemLabel(Image("first"))
                .tag(0)
            Text("Second View")
                .font(.title)
                .tabItemLabel(Image("second"))
                .tag(1)
        }
    }
}

#if DEBUG
struct ContentView_Previews : PreviewProvider {
    static var previews: some View {
        ContentView()
            .environmentObject(UserData(
                text: """
                        Some longer text here
                        that spans a few lines
                        and runs on.
                        """
            ))

    }
}
#endif

ここに画像の説明を入力してください


素晴らしい一時的な解決!純粋なSwiftUIを使用して解決できるまで、今のところ受け入れます。
gabriellanata

7
このソリューションでは、すでに改行が含まれているテキストを表示できますが、自然に長い行が途切れたり折り返されたりすることはないようです。(テキストは、フレームの外側の1行で水平方向に拡大し続けます。)長い行を折り返す方法について何かアイデアはありますか?
マイケル

6
(パブリッシャーでEnvironmentObjectの代わりに)Stateを使用し、それをMultilineTextViewへのバインディングとして渡すと、機能しないようです。変更を状態に反映するにはどうすればよいですか?
灰色

environmentObjectを使用せずにテキストビューにデフォルトのテキストを設定する方法はありますか?
Learn2Code

83

わかりました。@ sasアプローチから始めましたが、コンテンツに合わせた複数行のテキストフィールドなどのルックアンドフィールが本当に必要でした。これが私が得たものです。それが他の誰かに役立つことを願っています... Xcode11.1を使用しました。

提供されるカスタムMultilineTextFieldには次のものがあり
ます: 1。コンテンツフィット
2.オートフォーカス
3.プレースホルダー
4.コミット時

コンテンツに適合したswiftui複数行テキストフィールドのプレビュー プレースホルダーを追加

import SwiftUI
import UIKit

fileprivate struct UITextViewWrapper: UIViewRepresentable {
    typealias UIViewType = UITextView

    @Binding var text: String
    @Binding var calculatedHeight: CGFloat
    var onDone: (() -> Void)?

    func makeUIView(context: UIViewRepresentableContext<UITextViewWrapper>) -> UITextView {
        let textField = UITextView()
        textField.delegate = context.coordinator

        textField.isEditable = true
        textField.font = UIFont.preferredFont(forTextStyle: .body)
        textField.isSelectable = true
        textField.isUserInteractionEnabled = true
        textField.isScrollEnabled = false
        textField.backgroundColor = UIColor.clear
        if nil != onDone {
            textField.returnKeyType = .done
        }

        textField.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
        return textField
    }

    func updateUIView(_ uiView: UITextView, context: UIViewRepresentableContext<UITextViewWrapper>) {
        if uiView.text != self.text {
            uiView.text = self.text
        }
        if uiView.window != nil, !uiView.isFirstResponder {
            uiView.becomeFirstResponder()
        }
        UITextViewWrapper.recalculateHeight(view: uiView, result: $calculatedHeight)
    }

    fileprivate static func recalculateHeight(view: UIView, result: Binding<CGFloat>) {
        let newSize = view.sizeThatFits(CGSize(width: view.frame.size.width, height: CGFloat.greatestFiniteMagnitude))
        if result.wrappedValue != newSize.height {
            DispatchQueue.main.async {
                result.wrappedValue = newSize.height // !! must be called asynchronously
            }
        }
    }

    func makeCoordinator() -> Coordinator {
        return Coordinator(text: $text, height: $calculatedHeight, onDone: onDone)
    }

    final class Coordinator: NSObject, UITextViewDelegate {
        var text: Binding<String>
        var calculatedHeight: Binding<CGFloat>
        var onDone: (() -> Void)?

        init(text: Binding<String>, height: Binding<CGFloat>, onDone: (() -> Void)? = nil) {
            self.text = text
            self.calculatedHeight = height
            self.onDone = onDone
        }

        func textViewDidChange(_ uiView: UITextView) {
            text.wrappedValue = uiView.text
            UITextViewWrapper.recalculateHeight(view: uiView, result: calculatedHeight)
        }

        func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
            if let onDone = self.onDone, text == "\n" {
                textView.resignFirstResponder()
                onDone()
                return false
            }
            return true
        }
    }

}

struct MultilineTextField: View {

    private var placeholder: String
    private var onCommit: (() -> Void)?

    @Binding private var text: String
    private var internalText: Binding<String> {
        Binding<String>(get: { self.text } ) {
            self.text = $0
            self.showingPlaceholder = $0.isEmpty
        }
    }

    @State private var dynamicHeight: CGFloat = 100
    @State private var showingPlaceholder = false

    init (_ placeholder: String = "", text: Binding<String>, onCommit: (() -> Void)? = nil) {
        self.placeholder = placeholder
        self.onCommit = onCommit
        self._text = text
        self._showingPlaceholder = State<Bool>(initialValue: self.text.isEmpty)
    }

    var body: some View {
        UITextViewWrapper(text: self.internalText, calculatedHeight: $dynamicHeight, onDone: onCommit)
            .frame(minHeight: dynamicHeight, maxHeight: dynamicHeight)
            .background(placeholderView, alignment: .topLeading)
    }

    var placeholderView: some View {
        Group {
            if showingPlaceholder {
                Text(placeholder).foregroundColor(.gray)
                    .padding(.leading, 4)
                    .padding(.top, 8)
            }
        }
    }
}

#if DEBUG
struct MultilineTextField_Previews: PreviewProvider {
    static var test:String = ""//some very very very long description string to be initially wider than screen"
    static var testBinding = Binding<String>(get: { test }, set: {
//        print("New value: \($0)")
        test = $0 } )

    static var previews: some View {
        VStack(alignment: .leading) {
            Text("Description:")
            MultilineTextField("Enter some text here", text: testBinding, onCommit: {
                print("Final text: \(test)")
            })
                .overlay(RoundedRectangle(cornerRadius: 4).stroke(Color.black))
            Text("Something static here...")
            Spacer()
        }
        .padding()
    }
}
#endif

6
また、SwiftUIを使用してカスタム背景を有効backgroundColorUIColor.clearするようにUITextFieldを設定し、自動ファーストレスポンダーを削除することを検討する必要があります。これはMultilineTextFields、1つのビューで複数を使用すると壊れるためです(キーストロークごとに、すべてのテキストフィールドがレスポンダーを再度取得しようとします)。
iComputerfreak

2
@ kdion4891としてはで説明した別の質問からこの答え、あなただけ行うことができますtextField.textContainerInset = UIEdgeInsets.zero+textField.textContainer.lineFragmentPadding = 0とそれが述べたように、あなたが行う場合は、その後、削除する必要があります罰金👌🏻@Asperi作品.padding(.leading, 4).padding(.top, 8)、それ以外の場合は壊れて見ていきます。また、sのプレースホルダーの色に一致するように変更.foregroundColor(.gray)することもできます(ダークモードで更新されているかどうかは確認していません)。.foregroundColor(Color(UIColor.tertiaryLabel))TextField
レミB.19年

3
ああ、そして、私はまた、表示されたときに小さな「グリッチ」を修正するように変更@State private var dynamicHeight: CGFloat = 100@State private var dynamicHeight: CGFloat = UIFont.systemFontSizeましたMultilineTextField(それは短時間大きく表示されてから縮小します)。
レミB.19年

2
@ q8yas、以下に関連するコードにコメントまたは削除できますuiView.becomeFirstResponder
Asperi

3
コメントありがとうございます!本当に感謝しています。提供されているスナップショットは、特定の目的のために構成されたアプローチのデモです。あなたの提案はすべて正しいですが、あなたの目的のためです。このコードをコピーして貼り付け、目的に応じて自由に再構成できます。
Asperi

31

ではText()、あなたが使用してこれを達成することができ.lineLimit(nil)、およびドキュメントは、これが示唆する必要があるために働くTextField()すぎ。ただし、現在、これが期待どおりに機能していないことを確認できます。

バグが疑われます-フィードバックアシスタントでレポートを提出することをお勧めします。私はこれを行い、IDはFB6124711です。

編集:iOS 14のアップデート:TextEditor代わりに新しいものを使用してください。


ID FB6124711を使用してバグを検索する方法はありますか?フィードバックアシスタントをチェックしていますが、あまり役に立ちません
CrazyPro0 0719

私はそれをする方法があるとは思わない。しかし、あなたはあなたのレポートでそのIDに言及することができ、あなたのIDは同じ問題の重複であると説明しています。これは、トリアージチームが問題の優先度を上げるのに役立ちます。
AndrewEbling19年

2
これはまだXcodeのバージョンでは問題に11.0ベータ2(11M337n)で確認した
アンドリューEbling

3
これがXcodeバージョン11.0ベータ3(11M362v)の問題であることを確認しました。文字列を「Some \ ntext」に設定すると、2行で表示されますが、新しいコンテンツを入力すると、ビューのフレームの外側で1行のテキストが水平方向に拡大します。
マイケル

3
これはXcode11.4の問題です-真剣に??? このようなバグがある本番環境でSwiftUIをどのように使用することになっていますか。
Trev 1420

29

これにより、UITextViewがXcodeバージョン11.0ベータ6でラップされます(Xcode 11 GMシード2で引き続き機能します)。

import SwiftUI

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

       var body: some View {
        VStack {
            Text("text is: \(text)")
            TextView(
                text: $text
            )
                .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
        }

       }
}

struct TextView: UIViewRepresentable {
    @Binding var text: String

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

    func makeUIView(context: Context) -> UITextView {

        let myTextView = UITextView()
        myTextView.delegate = context.coordinator

        myTextView.font = UIFont(name: "HelveticaNeue", size: 15)
        myTextView.isScrollEnabled = true
        myTextView.isEditable = true
        myTextView.isUserInteractionEnabled = true
        myTextView.backgroundColor = UIColor(white: 0.0, alpha: 0.05)

        return myTextView
    }

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

    class Coordinator : NSObject, UITextViewDelegate {

        var parent: TextView

        init(_ uiTextView: TextView) {
            self.parent = uiTextView
        }

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

        func textViewDidChange(_ textView: UITextView) {
            print("text now: \(String(describing: textView.text!))")
            self.parent.text = textView.text
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

1
TextFieldは、Xcodeバージョン11.0(11A420a)、GMシード2、2019年9月
e9 8719

2
これはVStackでうまく機能しますが、リストを使用すると、行の高さが拡張されてTextViewのすべてのテキストが表示されません。変更:私はいくつかのことを試してみたisScrollEnabled中でTextView実施。TextViewフレームに固定幅を設定します。また、TextViewとTextをZStackに配置することもできますが(行がテキストビューの高さに一致するように拡張されることを期待して)、何も機能しません。この回答をリストでも機能するように適応させる方法について誰かアドバイスがありますか?
マシューズ

@Meo Fluteは、高さをコンテンツに一致させるために離れています。
アブドラ

isScrollEnabledをfalseに変更しましたが、動作します。
アブドラ

28

iOS 14

いわゆる TextEditor

struct ContentView: View {
    @State var text: String = "Multiline \ntext \nis called \nTextEditor"

    var body: some View {
        TextEditor(text: $text)
    }
}

動的成長高さ:

入力時に大きくしたい場合は、次のようなラベルを埋め込んでください。

ZStack {
    TextEditor(text: $text)
    Text(text).opacity(0).padding(.all, 8) // <- This will solve the issue if it is in the same ZStack
}

デモ

デモ


iOS 13

ネイティブUITextViewの使用

この構造体を使用して、SwiftUIコードでネイティブUITextViewを直接使用できます。

struct TextView: UIViewRepresentable {
    
    typealias UIViewType = UITextView
    var configuration = { (view: UIViewType) in }
    
    func makeUIView(context: UIViewRepresentableContext<Self>) -> UIViewType {
        UIViewType()
    }
    
    func updateUIView(_ uiView: UIViewType, context: UIViewRepresentableContext<Self>) {
        configuration(uiView)
    }
}

使用法

struct ContentView: View {
    var body: some View {
        TextView() {
            $0.textColor = .red
            // Any other setup you like
        }
    }
}

利点:

  • サポートiOSの13
  • レガシーコードと共有
  • で何年もテスト済み UIKit
  • 完全にカスタマイズ可能
  • オリジナルの他のすべての利点 UITextView

3
誰かがこの答えを見て、実際のテキストをTextView構造体に渡す方法を知りたい場合は、textColorを設定する行の下に次の行を追加します。$ 0.text = "Some text"
Mattl

1
テキストを変数にどのようにバインドしますか?または、テキストを取得しますか?
biomiker

1
最初のオプションにはすでにテキストバインディングがあります。2つ目は標準UITextViewです。UIKitで通常行うように操作できます。
MojtabaHosseini20年

13

現在、最善の解決策は、私が作成したTextViewというパッケージを使用することです

Swift Package Manager(READMEで説明)を使用してインストールできます。切り替え可能な編集状態と多数のカスタマイズが可能です(READMEでも詳しく説明されています)。

次に例を示します。

import SwiftUI
import TextView

struct ContentView: View {
    @State var input = ""
    @State var isEditing = false

    var body: some View {
        VStack {
            Button(action: {
                self.isEditing.toggle()
            }) {
                Text("\(isEditing ? "Stop" : "Start") editing")
            }
            TextView(text: $input, isEditing: $isEditing)
        }
    }
}

この例では、最初に2つの@State変数を定義します。1つは、TextViewが入力されるたびに書き込むテキスト用で、もう1つはTextViewのisEditing状態用です。

TextViewを選択すると、isEditing状態が切り替わります。ボタンをクリックするisEditingと、キーボードが表示される状態が切り替わり、の場合trueはTextViewが選択され、の場合はTextViewの選択が解除されfalseます。


1
リポジトリに問題を追加しますが、Asperiの元のソリューションと同様の問題があり、VStackではうまく機能しますが、ScrollViewでは機能しません。
RogerTheShrubber

No such module 'TextView'
アレックスBartiş

編集:あなたはMacOSのをターゲットにしているが、フレームワークは、唯一の理由UIViewRepresentableのUIKitのサポート
アレックスBartiş

11

@Meo Fluteの答えは素晴らしいです!ただし、多段テキスト入力では機能しません。そして、@ Asperiの回答と組み合わせて、これを修正しました。また、楽しみのためにプレースホルダーのサポートも追加しました。

struct TextView: UIViewRepresentable {
    var placeholder: String
    @Binding var text: String

    var minHeight: CGFloat
    @Binding var calculatedHeight: CGFloat

    init(placeholder: String, text: Binding<String>, minHeight: CGFloat, calculatedHeight: Binding<CGFloat>) {
        self.placeholder = placeholder
        self._text = text
        self.minHeight = minHeight
        self._calculatedHeight = calculatedHeight
    }

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

    func makeUIView(context: Context) -> UITextView {
        let textView = UITextView()
        textView.delegate = context.coordinator

        // Decrease priority of content resistance, so content would not push external layout set in SwiftUI
        textView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)

        textView.isScrollEnabled = false
        textView.isEditable = true
        textView.isUserInteractionEnabled = true
        textView.backgroundColor = UIColor(white: 0.0, alpha: 0.05)

        // Set the placeholder
        textView.text = placeholder
        textView.textColor = UIColor.lightGray

        return textView
    }

    func updateUIView(_ textView: UITextView, context: Context) {
        textView.text = self.text

        recalculateHeight(view: textView)
    }

    func recalculateHeight(view: UIView) {
        let newSize = view.sizeThatFits(CGSize(width: view.frame.size.width, height: CGFloat.greatestFiniteMagnitude))
        if minHeight < newSize.height && $calculatedHeight.wrappedValue != newSize.height {
            DispatchQueue.main.async {
                self.$calculatedHeight.wrappedValue = newSize.height // !! must be called asynchronously
            }
        } else if minHeight >= newSize.height && $calculatedHeight.wrappedValue != minHeight {
            DispatchQueue.main.async {
                self.$calculatedHeight.wrappedValue = self.minHeight // !! must be called asynchronously
            }
        }
    }

    class Coordinator : NSObject, UITextViewDelegate {

        var parent: TextView

        init(_ uiTextView: TextView) {
            self.parent = uiTextView
        }

        func textViewDidChange(_ textView: UITextView) {
            // This is needed for multistage text input (eg. Chinese, Japanese)
            if textView.markedTextRange == nil {
                parent.text = textView.text ?? String()
                parent.recalculateHeight(view: textView)
            }
        }

        func textViewDidBeginEditing(_ textView: UITextView) {
            if textView.textColor == UIColor.lightGray {
                textView.text = nil
                textView.textColor = UIColor.black
            }
        }

        func textViewDidEndEditing(_ textView: UITextView) {
            if textView.text.isEmpty {
                textView.text = parent.placeholder
                textView.textColor = UIColor.lightGray
            }
        }
    }
}

次のように使用します。

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

    var body: some View {
        ScrollView {
            TextView(placeholder: "", text: self.$text, minHeight: self.textHeight, calculatedHeight: self.$textHeight)
            .frame(minHeight: self.textHeight, maxHeight: self.textHeight)
        }
    }
}

私はこれが好き。プレースホルダーが機能していないようですが、最初から始めると便利でした。ライトモードとダークモードの両方がサポートされるように、UIColor.lightGrayの代わりにUIColor.tertiaryLabel、UIColor.blackの代わりにUIColor.labelなどのセマンティックカラーを使用することをお勧めします。
ヘラム

@Helamプレースホルダーがどのように機能していないかを説明してもよろしいですか?
DanielTseng20年

@DanielTsengは表示されません。それはどのように動作するはずですか?テキストが空かどうかが表示されることを期待していましたが、表示されません。
ヘラム

@Helam、私の例では、空のプレースホルダーがあります。他のものに変えてみましたか?( ""ではなく "Hello World!")
Daniel Tseng 2010年

はい、私の場合は別のものに設定しました。
ヘラム

3

SwiftUI TextView(UIViewRepresentable)で、次のパラメーターを使用できます:fontStyle、isEditable、backgroundColor、borderColor、border Width

TextView(text:self。$ viewModel.text、fontStyle:.body、isEditable:true、backgroundColor:UIColor.white、borderColor:UIColor.lightGray、borderWidth:1.0).padding()

TextView(UIViewRepresentable)

struct TextView: UIViewRepresentable {

@Binding var text: String
var fontStyle: UIFont.TextStyle
var isEditable: Bool
var backgroundColor: UIColor
var borderColor: UIColor
var borderWidth: CGFloat

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

func makeUIView(context: Context) -> UITextView {

    let myTextView = UITextView()
    myTextView.delegate = context.coordinator

    myTextView.font = UIFont.preferredFont(forTextStyle: fontStyle)
    myTextView.isScrollEnabled = true
    myTextView.isEditable = isEditable
    myTextView.isUserInteractionEnabled = true
    myTextView.backgroundColor = backgroundColor
    myTextView.layer.borderColor = borderColor.cgColor
    myTextView.layer.borderWidth = borderWidth
    myTextView.layer.cornerRadius = 8
    return myTextView
}

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

class Coordinator : NSObject, UITextViewDelegate {

    var parent: TextView

    init(_ uiTextView: TextView) {
        self.parent = uiTextView
    }

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

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

}


複数行のテキストフィールドにデフォルトのテキストを追加するにはどうすればよいですか?
Learn2Code

3

Xcode 12iOS14で利用可能で、とても簡単です。

import SwiftUI

struct ContentView: View {
    
    @State private var text = "Hello world"
    
    var body: some View {
        TextEditor(text: $text)
    }
}

uは、ユーザがiOS13にまだあるものならばiOS14、で作業している場合にのみ、このイマイチ
ディオタク

3

MacOSの実装

struct MultilineTextField: NSViewRepresentable {
    
    typealias NSViewType = NSTextView
    private let textView = NSTextView()
    @Binding var text: String
    
    func makeNSView(context: Context) -> NSTextView {
        textView.delegate = context.coordinator
        return textView
    }
    func updateNSView(_ nsView: NSTextView, context: Context) {
        nsView.string = text
    }
    func makeCoordinator() -> Coordinator {
        return Coordinator(self)
    }
    class Coordinator: NSObject, NSTextViewDelegate {
        let parent: MultilineTextField
        init(_ textView: MultilineTextField) {
            parent = textView
        }
        func textDidChange(_ notification: Notification) {
            guard let textView = notification.object as? NSTextView else { return }
            self.parent.text = textView.string
        }
    }
}

と使用方法

struct ContentView: View {

    @State var someString = ""

    var body: some View {
         MultilineTextField(text: $someString)
    }
}

0

TextEditor(text: $text)高さなどの修飾子を使用して追加するだけです。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.