Reputation: 1
I wanted to have some TextEditor
in my ForEach and I made this sample code in down! As you can see the code and result of it in Image, TextEditor
act like a greedy view, and takes all available space! which has so many downsides for me at least!
If I go and limit the hight to a custom value then I would loss the possibility of seeing all strings and lines of strings of TextEditor in itself and I must scroll up or down for seeing other lines, which is not my design!
My goal is that the TextEditor
takes the space as it needs and if I enter new line of string then it can grow in height or if I remove the lines of strings it can shrinks in height to minimum of 1 line at least!
I wonder how can I do this?
struct ContentView: View {
@StateObject var textEditorReferenceType: TextEditorReferenceType = TextEditorReferenceType()
var body: some View {
Text("TextEditorView").bold().padding()
VStack {
ForEach(textEditorReferenceType.arrayOfString.indices, id: \.self, content: { index in
TextEditorView(string: $textEditorReferenceType.arrayOfString[index])
})
}
.padding()
}
}
struct TextEditorView: View {
@Binding var string: String
var body: some View {
TextEditor(text: $string)
.cornerRadius(10.0)
.shadow(radius: 1.0)
}
}
class TextEditorReferenceType: ObservableObject {
@Published var arrayOfString: [String] = ["Hello, World!", "Hello, World!", "Hello, World!"]
}
Upvotes: 24
Views: 19080
Reputation: 30378
TextEditor
, not TextField
!)Contrary to some of the answers here, as others have pointed out, the OP specifically asked about TextEditor
, not TextField
. You need TextEditor
if you want to support explicitly adding new-lines during text entry, not just wrapping a single line to span many.
The easiest solution for achieving this with TextEditor
requires two things:
.fixedSize(horizontal: false, vertical: true)
on the TextEditor
(to dictate the absolute minimum height)Here's an example where I have a Button
directly underneath an auto-growing TextEditor
. I'm using Color.clear
with an infinite frame to make it greedy, then .layoutPriority(1)
to make it overpower the greediness of the TextEditor
control. The fixedSize
on that TextEditor
says 'Yo, you can't collapse past my text, brah!!', thus that's as far as it smooshes to. Without fixedSize
, it would collapse to a height of zero.
struct TestApp: App {
static private let initialText = """
He: Tell me a joke!
She: Ok... what do you call two crows sitting on a branch?
He: I dunno, what?
She: Attempted murder!
"""
@State private var text: String = Self.initialText
var body: some Scene {
WindowGroup {
VStack(alignment: .leading) {
TextEditor(text: $text)
.fixedSize(horizontal: false, vertical: true)
Button("Test Me") {
text = Self.initialText
}
Color.clear
.frame(maxWidth: .infinity, maxHeight: .infinity)
.layoutPriority(1)
}
.padding()
}
}
}
Side-note: As mentioned above,
layoutPriority
only affects layout in the current container so make sure the greedy control it's applied to is either a direct sibling of yourTextEditor
(i.e. they have the same/immediate parent) or theTextEditor
is further down the layout, not above.This also means if you nest the below
VStack
inside anotherVStack
with some other controls, the children of that outerVStack
--including your innerVStack
--will again be distributed equally within it (unless of course you applylayoutPriority
to any of those controls too.)
As mentioned above, I use an explicit greedy spacer control there: Color.clear
with the frame. However, technically it isn't needed as you can add a frame directly to the button to achieve the same thing. You just need to also specify the appropriate alignment
value to say where you want that button to end up in the frame's resulting greedy area. Here that's .topLeading
so the button ends up directly underneath the TextEditor
on the left side. If you don't add the alignment
, the button would end up in the middle of the greedy area as .center
is the default alignment.
That said, I personally prefer explicit layouts so the spacer is my choice, but others may prefer the simplicity of not needing a dedicated control just for that.*
// `alignment: topLeading` in the frame puts the button in the top-left of the 'greedy' area that the frame creates
Button("Test Me") {
text = Self.initialText
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
.layoutPriority(1)
And here are examples of the results...
Upvotes: 10
Reputation: 605
Or you could just, y'know, use a Form
guys:
struct JustUseAForm: View {
@State var text1: String = "Enter Text Here"
@State var text2: String = "Enter Text Here"
@State var text3: String = "Enter Text Here"
var body: some View {
Form {
Group {
VStack(alignment: .leading) {
Spacer(minLength: 8)
Text("Comment 1:")
TextEditor(text: $text1)
.padding(.all, 1.0)
}
VStack(alignment: .leading) {
Spacer(minLength: 8)
Text("Comment 2:")
TextEditor(text: $text2)
.padding(.all, 1.0)
}
VStack(alignment: .leading) {
Spacer(minLength: 8)
Text("Comment 3:")
TextEditor(text: $text3)
.padding(.all, 1.0)
}
}
.padding(10)
.background(Color(.sRGB, red: 0.9, green: 0.9, blue: 0.9, opacity: 0.9))
.cornerRadius(20)
}
}
}
Example:
Of course, this means you have to be OK with the default way Forms render, because just like .sheet
and most other things in SwiftUI, Apple gives us no way to customize the appearance. You either like what they give us, or you figure out a bizarre hack, or you wrap a UIKit implementation.
Fun times.
Maybe in the comments someone can explain for us why TextEditor
magically works properly within a Form
but not anywhere else? Or why scaleToFit()
does not work properly with TextEditor
? Or why lineLimit
does not work properly with TextEditor
? So many questions, so few answers, unless you work at Apple, I guess.
Upvotes: 0
Reputation: 542
Adding .fixedSize(horizontal: false, vertical: true)
and a minimum height solved the issue for me.
Example :
TextEditor(text: $myBinding)
.frame(minHeight: 50)
.fixedSize(horizontal: false, vertical: true)
Upvotes: 24
Reputation: 1247
In iOS 16 it's now natively possible with a regular textField
by adding axis: .vertical
and .lineLimit()
linelimit defines the number of lines until the textfield extends. Add a range to start to define a range of lines within the textfield will start and stop to extend.
WWDC22 Session "What'S new in SwiftUI around 17:10
Upvotes: 39
Reputation: 52387
You can use a PreferenceKey
and an invisible Text
overlay to measure the string dimensions and set the TextEditor
's frame:
struct TextEditorView: View {
@Binding var string: String
@State var textEditorHeight : CGFloat = 20
var body: some View {
ZStack(alignment: .leading) {
Text(string)
.font(.system(.body))
.foregroundColor(.clear)
.padding(14)
.background(GeometryReader {
Color.clear.preference(key: ViewHeightKey.self,
value: $0.frame(in: .local).size.height)
})
TextEditor(text: $string)
.font(.system(.body))
.frame(height: max(40,textEditorHeight))
.cornerRadius(10.0)
.shadow(radius: 1.0)
}.onPreferenceChange(ViewHeightKey.self) { textEditorHeight = $0 }
}
}
struct ViewHeightKey: PreferenceKey {
static var defaultValue: CGFloat { 0 }
static func reduce(value: inout Value, nextValue: () -> Value) {
value = value + nextValue()
}
}
Adapted from my other answer here: Mimicking behavior of iMessage with TextEditor for text entry
Upvotes: 31