How to Create a Multiline Textfield in Swiftui

How do I create a multiline TextField in SwiftUI?

Update: While Xcode11 beta 4 now does support TextView, I've found that wrapping a UITextView is still be best way to get editable multiline text to work. For instance, TextView has display glitches where text does not appear properly inside the view.

Original (beta 1) answer:

For now, you could wrap a UITextView to create a composable 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

Sample Image

How do I create a multiline Textfiled in SwiftUI?

Here is a 90% solution. It deletes further text after the 3rd line break, but it cannot detect line wraps in long texts.

struct ContentView: View {

@State private var notes = ""

var body: some View {

VStack (alignment: .leading) {
Text("Notes")
.fontWeight(.bold)

CustomTextEditor("Enter your note", text: $notes)
}
.padding()

}
}


struct CustomTextEditor: View {

init(_ prompt: LocalizedStringKey, text: Binding<String>) {
self.prompt = prompt
self._text = Binding(projectedValue: text)
}
let prompt: LocalizedStringKey
@Binding var text: String

var body: some View {
ZStack(alignment: .topLeading ) {

Text(prompt)
.foregroundColor(text == "" ? .secondary : .clear)
.padding(EdgeInsets(top: 7, leading: 3, bottom: 0, trailing: 0))

TextEditor(text: $text)

// deleting everything after the 3rd linebreak
.onChange(of: text) { _ in
let stringArray = text.map { $0 }
let pos = stringArray.indices.filter { stringArray[$0] == "\n"}
if pos.count > 2 {
text = String(stringArray.prefix(upTo: pos[2]))
}
}
}
.frame(height: 82)
.padding(10)
.background(Color(.systemGray5))
.cornerRadius(20)

// getting rid of TextEditor standard background
.onAppear {
UITextView.appearance().backgroundColor = .clear
}
.onDisappear {
UITextView.appearance().backgroundColor = .systemGray5
}
}
}

Make the TextField Multiline - SwiftUI

Since iOS 16

The lineLimit modifier works as expected if you choose .vertical value for the axis parameter. And it also supports range now:

TextField("Title", text: $text, axis: .vertical)
.lineLimit(5...10)

Demo



Since iOS 14

Form iOS 14 and with Xcode 12, it's available as TextEditor

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

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


iOS 13

Also, if you want to support iOS 13, you can take at look this answer to port UITextField inside SwiftUI with full access to all of its properties.

How do I create a Multi Line Text Field in SwiftUI for MacOS?

Here is some initial demo of component like iOS14 TextEditor.

Demo prepared & tested with Xcode 11.7 / macOS 10.15.6

demo

struct TestTextArea: View {
@State private var text = "Placeholder: Enter some text"

var body: some View {
VStack {
TextArea(text: $text)
.border(Color.black)
// Text(text) // uncomment to see mirror of enterred text
}.padding()
}
}

struct TextArea: NSViewRepresentable {
@Binding var text: String

func makeNSView(context: Context) -> NSScrollView {
context.coordinator.createTextViewStack()
}

func updateNSView(_ nsView: NSScrollView, context: Context) {
if let textArea = nsView.documentView as? NSTextView, textArea.string != self.text {
textArea.string = self.text
}
}

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

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

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

func textView(_ textView: NSTextView, shouldChangeTextIn range: NSRange, replacementString text: String?) -> Bool {
defer {
self.text.wrappedValue = (textView.string as NSString).replacingCharacters(in: range, with: text!)
}
return true
}

fileprivate lazy var textStorage = NSTextStorage()
fileprivate lazy var layoutManager = NSLayoutManager()
fileprivate lazy var textContainer = NSTextContainer()
fileprivate lazy var textView: NSTextView = NSTextView(frame: CGRect(), textContainer: textContainer)
fileprivate lazy var scrollview = NSScrollView()

func createTextViewStack() -> NSScrollView {
let contentSize = scrollview.contentSize

textContainer.containerSize = CGSize(width: contentSize.width, height: CGFloat.greatestFiniteMagnitude)
textContainer.widthTracksTextView = true

textView.minSize = CGSize(width: 0, height: 0)
textView.maxSize = CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude)
textView.isVerticallyResizable = true
textView.frame = CGRect(x: 0, y: 0, width: contentSize.width, height: contentSize.height)
textView.autoresizingMask = [.width]
textView.delegate = self

scrollview.borderType = .noBorder
scrollview.hasVerticalScroller = true
scrollview.documentView = textView

textStorage.addLayoutManager(layoutManager)
layoutManager.addTextContainer(textContainer)

return scrollview
}
}
}

backup

SwiftUI: How do I make TextField fit multi-line content?

It seems there's no direct argument to manage multiline padding correctly. They are maybe underdevelopping. But the following will give you a straight workaround solution to what you are expecting.

extension String{
var extraLines : String{ get{
return self + String(repeating:"\n", count: self.components(separatedBy: "\n").count - 1)
}}
}


struct TextFieldDemo: View {
var content: Binding<String>

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

@State var height : CGFloat? //current height

let constHeightRatio : CGFloat = 0.55 //use for assembly with other fonts.
let defaultHeight : CGFloat = 250 //use for assembly with other views.

var body: some View {
TextField("Custom placeholder", text: content).environment(\.multilineTextAlignment, .center).alignmentGuide(.bottom) { (ViewDimensions) -> CGFloat in
if self.height == nil {self.height = ViewDimensions.height}
return ViewDimensions.height
}.frame( height: (height ?? defaultHeight) * constHeightRatio, alignment: .bottom).background(Color.yellow)
}
}



#if DEBUG
struct TextInputRowPreviews: PreviewProvider {

static var previews: some View {
let content = "content\ncontent\ncontent".extraLines
return
TextFieldDemo(content: .constant(content))
}
}
#endif

This works fine for single view. If view assembly is required (with other stacking views, etc), you may adjust defaultHeight and/or constHeightRatio to achieve what you want. Hopefully it works for you too.



Related Topics



Leave a reply



Submit