Як створити багаторядковий TextField у SwiftUI?


85

Я намагався створити багаторядковий TextField у SwiftUI, але не можу зрозуміти, як.

Це код, який я зараз маю:

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
Я просто спробував створити багаторядкове текстове поле з swiftui у версії Xcode 11.0 (11A419c), GM, використовуючи lineLimit (). Це все ще не працює. Не можу повірити, що Apple цього ще не виправила. Багаторядкове текстове поле досить поширене у мобільних додатках.
e987,

Відповіді:


45

Оновлення: Хоча Xcode11 beta 4 тепер підтримує TextView, я виявив, що обгортання a UITextViewвсе ще є найкращим способом примусити редагований багаторядковий текст працювати. Наприклад, TextViewмає збої на дисплеї, коли текст не відображається належним чином у поданні.

Оригінальна (бета-версія 1) відповідь:

Наразі ви можете обернути a, UITextViewщоб створити компонуючий 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
Це рішення дозволяє відображати текст, у якому вже є нові рядки, але, здається, він не ламає / не обгортає довгі рядки від природи. (Текст просто продовжує зростати горизонтально на одному рядку, поза рамкою.) Будь-які ідеї, як змусити довгі рядки обертати?
Майкл

5
Якщо я використовую State (замість EnvironmentObject з Publisher) і передаю його як прив'язку до MultilineTextView, це, здається, не працює. Як я можу відобразити зміни в штаті?
сірий

Чи є спосіб встановити за замовчуванням текст у текстовому режимі без використання середовищаObject?
Learn2Code

76

Гаразд, я почав із підходу @sas, але потребував, щоб це справді виглядало та відчувалось як багаторядкове текстове поле із вмістом вмісту тощо. Ось що я маю. Сподіваюся, це буде корисно комусь іншому ... Використовується Xcode 11.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
Також вам слід подумати про налаштування backgroundColorUITextField для UIColor.clearввімкнення нестандартних фонів за допомогою SwiftUI та про вилучення автовідповідача, оскільки він ламається при використанні декількох MultilineTextFieldsв одному поданні (кожне натискання клавіші, усі текстові поля намагаються отримати відповідь знову).
iComputerfreak

2
@ Kdion4891 Як пояснено в цій відповіді від іншого питання , ви можете просто зробити textField.textContainerInset = UIEdgeInsets.zero+ textField.textContainer.lineFragmentPadding = 0і працює відмінно 👌🏻 @Asperi Якщо ви , як уже згадувалося, вам потрібно буде видалити .padding(.leading, 4)і в .padding(.top, 8)іншому випадку він буде виглядати зламана. Крім того, ви можете змінити .foregroundColor(.gray)для .foregroundColor(Color(UIColor.tertiaryLabel))відповідності кольору заповнювачів у TextFields (я не перевіряв, чи оновлюється темний режим).
Ремі Б.

3
О, і я також змінив @State private var dynamicHeight: CGFloat = 100для @State private var dynamicHeight: CGFloat = UIFont.systemFontSizeвиправити невеликий «глюк» , коли MultilineTextFieldз'являється (це показує велику за короткий час , а потім стискається).
Ремі Б.

2
@ q8yas, ти можеш коментувати або видаляти код, що стосуєтьсяuiView.becomeFirstResponder
Аспері

3
Дякуємо всім за коментарі! Я дуже ціную це. Наданий знімок - це демонстраційний підхід, який було налаштовано для конкретної мети. Усі ваші пропозиції правильні, але для ваших цілей. Ви можете скопіювати та вставити цей код і переналаштувати його настільки, наскільки бажаєте для своєї мети.
Аспері

28

Це обгортає UITextView у Xcode версії 11.0 beta 6 (все ще працює на Xcode 11 GM seed 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 все ще не впливає lineLimit () у версії Xcode 11.0 (11A420a), GM Seed 2, вересень, 2019
e987,

2
Це добре працює у VStack, але при використанні списку висота рядка не розширюється, щоб показати весь текст у TextView. Я спробував кілька речей: зміни isScrollEnabledу TextViewреалізації; встановлення фіксованої ширини на рамці TextView; і навіть розміщення TextView і Text в ZStack (в надії, що рядок розшириться, щоб відповідати висоті подання Text), але нічого не працює. Хто-небудь має пораду, як адаптувати цю відповідь, щоб вона також працювала у списку?
MathewS

@Meo Flute є там, щоб висота відповідала вмісту.
Абдулла

Я змінив isScrollEnabled на false, і це працює, дякую.
Абдулла

26

За допомогою a Text()ви можете досягти цього за допомогою .lineLimit(nil), і документація вказує, що це теж має працювати TextField(). Однак я можу підтвердити, що наразі це не працює належним чином.

Я підозрюю помилку - радив би подати звіт у Feedback Assistant. Я зробив це, і ідентифікатор - FB6124711.

EDIT: Оновлення для iOS 14: TextEditorзамість цього використовуйте нове .


Чи є спосіб шукати помилку за допомогою ідентифікатора FB6124711? Оскільки я перевіряю помічника з відгуками, але це не надто корисно
CrazyPro007

Я не вірю, що є спосіб зробити це. Але ви могли б згадати цей ідентифікатор у своєму звіті, пояснюючи свій, це обман того самого питання. Це допомагає команді сортування підняти пріоритет питання.
Ендрю

2
Підтверджено, що це все ще проблема в Xcode версії 11.0 beta 2 (11M337n)
Ендрю

3
Підтверджено, що це все ще проблема в Xcode версії 11.0 beta 3 (11M362v). Ви можете встановити для рядка "Some \ ntext", і він відображатиметься у двох рядках, але введення нового вмісту призведе до того, що один рядок тексту буде рости горизонтально, поза рамкою вашого перегляду.
Майкл

3
Це все ще проблема в Xcode 11.4 - Серйозно ??? Як ми повинні використовувати SwiftUI у виробництві з такими помилками.
Trev14,

16

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

Ви можете використовувати власний UITextView прямо в коді SwiftUI з такою структурою:

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 = "Трохи тексту"
Mattl

1
Як ви прив'язуєте текст до змінної? Або інакше отримати текст?
biomiker

1
Перший варіант вже має прив'язку тексту. Другий - стандарт UITextView. Ви можете взаємодіяти з ним, як зазвичай це робиться в UIKit.
Мойтаба Хоссейні,

12

В даний час найкращим рішенням є використання цього пакета, який я створив під назвою 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)
        }
    }
}

У цьому прикладі ви спочатку визначаєте дві @Stateзмінні. Один призначений для тексту, куди TextView пише, коли його вводять, а інший - для isEditingстану TextView.

Якщо вибрано TextView, він перемикає isEditingстан. Коли ви натискаєте кнопку, це також перемикає isEditingстан, який відображатиме клавіатуру та вибиратиме TextView коли trueі скасовує вибір TextView коли false.


1
Я додаю проблему з репо, але у нього проблема, схожа на оригінальне рішення Asperi, вона чудово працює у VStack, але не в ScrollView.
RogerTheShrubber

No such module 'TextView'
Алекс Бартіш,

Редагувати: ви націлюєтесь на macOS, але фреймворк підтримує лише UIKit через UIViewRepresentable
Alex Bartiş

10

Відповідь @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.tertiaryLabel замість UIColor.lightGray та UIColor.label замість UIColor.black, щоб підтримувався як світлий, так і темний режим.
Гелам,

@Helam Вам не шкода пояснити, як заповнювач не працює?
Даніель Ценг,

@DanielTseng це не відображається. Як це повинно поводитися? Я очікував, що це покаже, якщо текст порожній, але він ніколи не відображається для мене.
Гелам

@Helam, У моєму прикладі у мене заповнювач повинен бути порожнім. Ви пробували змінити його на щось інше? ("Привіт Світ!" Замість "")
Даніель Ценг

Так у своєму я встановив, що це щось інше.
Гелам

2

SwiftUI TextView (UIViewRepresentable) з наступними доступними параметрами: fontStyle, isEditable, backgroundColor, borderColor & Width border

TextView (текст: 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
    }
}

}


1

Доступно для Xcode 12 та iOS14 , це дуже просто.

import SwiftUI

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

хіба це лише в тому випадку, якщо ви працюєте з iOS14, а якщо користувач все ще працює на iOS13
Di Nerd

1

Впровадження 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)
    }
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.