Как создать многострочное текстовое поле в SwiftUI?


93

Я пытался создать многострочное текстовое поле в 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

Ответы:


49

Обновление: хотя Xcode11 beta 4 теперь поддерживает TextView, я обнаружил, что упаковка по- 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
Это решение позволяет отображать текст, в котором уже есть символы новой строки, но оно не разбивает / не переносит естественные длинные строки. (Текст просто продолжает расти по горизонтали на одной строке за пределами кадра.) Есть идеи, как сделать длинные строки для переноса?
Майкл

6
Если я использую State (вместо EnvironmentObject с издателем) и передаю его как привязку к MultilineTextView, похоже, это не работает. Как я могу отразить изменения обратно в состояние?
серый

Есть ли способ установить текст по умолчанию в текстовом представлении без использования environmentObject?
Learn2Code

83

Хорошо, я начал с подхода @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))чтобы соответствовать цвету заполнителей в TextFieldс (я не проверял , если , если обновления в режиме темного).
Реми Б.

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

2
@ q8yas, вы можете прокомментировать или удалить код, связанный сuiView.becomeFirstResponder
Asperi 01

3
Спасибо всем за комментарии! Я это очень ценю. Приведенный снимок представляет собой демонстрацию подхода, настроенного для определенной цели. Все ваши предложения верны, но для ваших целей. Вы можете скопировать и вставить этот код и перенастроить его настолько, насколько хотите для своих целей.
Аспери

31

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

Подозреваю ошибку - рекомендую заполнить отчет с помощью Feedback Assistant. Я сделал это, и ID - FB6124711.

РЕДАКТИРОВАТЬ: Обновление для 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

29

Это обертывает 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, но при использовании List высота строки не увеличивается, чтобы показать весь текст в TextView. Я попробовал несколько вещей: изменения isScrollEnabledв TextViewреализации; установка фиксированной ширины рамки TextView; и даже помещая TextView и Text в ZStack (в надежде, что строка расширится до соответствия высоте текстового представления), но ничего не работает. Есть ли у кого-нибудь совет о том, как адаптировать этот ответ для работы в списке?
MathewS

@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

вы можете использовать собственный 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 = "Some text"
Mattl

1
Как связать текст с переменной? Или иначе получить текст?
biomiker

1
Первый вариант уже имеет текстовую привязку. Второй - стандартный UITextView. Вы можете взаимодействовать с ним, как обычно в UIKit.
Mojtaba Hosseini

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)
        }
    }
}

В этом примере вы сначала определяете две @Stateпеременные. Один предназначен для текста, который TextView записывает всякий раз, когда он вводится, а другой - дляisEditing состояния TextView.

TextView, если он выбран, переключает isEditing состояние. Когда вы нажимаете кнопку, это также переключает isEditingсостояние, которое будет отображать клавиатуру и выбрать TextView, когда true, и отменить выбор TextView, когда false.


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

No such module 'TextView'
Alex Bartiş

Изменить: вы нацеливаетесь на macOS, но фреймворк поддерживает только UIKit из-за UIViewRepresentable
Алекс Бартиш

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.tertiaryLabel вместо UIColor.lightGray и UIColor.label вместо UIColor.black, чтобы поддерживались как светлый, так и темный режим.
Helam

@Helam Не могли бы вы объяснить, как не работает заполнитель?
Даниэль Ценг

@DanielTseng не появляется. Как он должен себя вести? Я ожидал, что он покажет, пуст ли текст, но он никогда не отображается для меня.
Helam

@Helam, в моем примере заполнитель должен быть пустым. Вы пробовали поменять его на что-нибудь другое? («Hello World!» Вместо «»)
Даниэль Ценг

Да, в моем я установил что-то другое.
Helam

3

SwiftUI TextView (UIViewRepresentable) со следующими доступными параметрами: fontStyle, isEditable, backgroundColor, borderColor и ширина границы.

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
    }
}

}


Как мне добавить текст по умолчанию в многострочное текстовое поле?
Learn2Code

3

Доступно для Xcode 12 и iOS14 , это действительно просто.

import SwiftUI

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

Разве это не только в том случае, если вы работаете с iOS14, а что, если пользователь все еще использует iOS13,
Ди Нерд

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)а затем добавить любые модификаторы для таких вещей, как высота.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.