Controlling Size of Texteditor in Swiftui

Controlling size of TextEditor in SwiftUI

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

Make TextEditor dynamic height SwiftUI

I found a solution!

For everyone else trying to solve this:

I added a Text with the same width of the input field and then used a GeometryReader to calculate the height of the Text which automatically wraps. Then if you divide the height by the font size you get the number of lines.

You can make the text field hidden (tested in iOS 14 and iOS 15 beta 3)

Dynamic height for TextEditor

You can put it in a ZStack with an invisible Text for sizing:

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

TextEditor sticking to minHeight inSwiftUI

Here is possible solution. Tested with Xcode 12 / iOS 14.

demo

ScrollView {
// make clear static text in background to define size and
// have TextEditor in front with same text fit
Text(loremIpsum).foregroundColor(.clear).padding(8)
.frame(maxWidth: .infinity)
.overlay(
TextEditor(text: .constant(loremIpsum))
)
}
.frame(minHeight: 200.0)
.border(Color.yellow, width: 3.0)

Dynamic row hight containing TextEditor inside a List in SwiftUI

You can use an invisible Text in a ZStack to make it dynamic.

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

var body: some View {
List((1...10), id: \.self) { _ in
ZStack {
TextEditor(text: $text)
Text(text).opacity(0).padding(.all, 8) // <- This will solve the issue if it is in the same ZStack
}
}
}
}

Note that you should consider changing font size and other properties to match the TextEditor

Preview

SwiftUI TextEditor Divider doesn't change Y position based on text-line count?

It is doing exactly what you told it to do. But a background color on your TextEditor. You will see that it has a height of 200 + a spacing of 10 from the VStack.

I changed your code to make it obvious:

struct EditBio: View {
@State var editProfileVM = ""

var body: some View {
VStack(spacing: 10) {
TextEditor(text: $editProfileVM)
.foregroundColor(.white)
.padding(.top, 70)
.padding([.leading, .trailing], 50)
.frame(minWidth: 100, idealWidth: 200, maxWidth: 400, maxHeight: 200, alignment: .center)
.background(Color.gray)
Divider().frame(height: 1).background(.red)
Spacer()
}
}
}

to produce this:

Sample Image

You can see the TextEditor naturally wants to be taller than 200, but that is limiting it. Therefore, the Spacer() is not going to cause the TextEditor to be any smaller.

The other problem that setting a fixed frame causes will be that your text will end up off screen at some point. I am presuming what you really want is a self sizing TextEditor that is no larger than it's contents.

That can be simply done with the following code:

struct EditBio: View {
@State var editProfileVM = ""
var body: some View {
VStack(spacing: 10) {
SelfSizingTextEditor(text: $editProfileVM)
// Frame removed for the image below.
// .frame(minWidth: 100, idealWidth: 200, maxWidth: 400, maxHeight: 200, alignment: .center)
.foregroundColor(.white)
// made the .top padding to be .vertical
.padding(.vertical, 70)
.padding([.leading, .trailing], 50)
.background(Color.gray)
Divider().frame(height: 5).background(.red)
Spacer()
}
}
}

struct SelfSizingTextEditor: View {

@Binding var text: String
@State var textEditorSize = CGSize.zero

var body: some View {
ZStack {
Text(text)
.foregroundColor(.clear)
.copySize(to: $textEditorSize)
TextEditor(text: $text)
.frame(height: textEditorSize.height)
}
}
}

extension View {
func readSize(onChange: @escaping (CGSize) -> Void) -> some View {
background(
GeometryReader { geometryProxy in
Color.clear
.preference(key: SizePreferenceKey.self, value: geometryProxy.size)
}
)
.onPreferenceChange(SizePreferenceKey.self, perform: onChange)
}

func copySize(to binding: Binding<CGSize>) -> some View {
self.readSize { size in
binding.wrappedValue = size
}
}
}

producing this view:

Sample Image

SwiftUI TextEditor Initial Content Size Is Wrong

Note: views are left semi-transparent with borders so you can see/debug what's going on.

struct FieldView: View{
@Binding var name: String
@State private var textEditorHeight : CGFloat = 100
var body: some View{
ZStack(alignment: .topLeading) {
Text(name)
.background(GeometryReader {
Color.clear
.preference(key: ViewHeightKey.self,
value: $0.frame(in: .local).size.height)

})
//.opacity(0)
.border(Color.pink)
.foregroundColor(Color.red)

TextEditor(text: $name)
.padding(EdgeInsets(top: -7, leading: -3, bottom: -5, trailing: -7))
.frame(height: textEditorHeight + 12)
.border(Color.green)
.opacity(0.4)
}
.onPreferenceChange(ViewHeightKey.self) { textEditorHeight = $0 }
}
}

struct ViewHeightKey: PreferenceKey {
static var defaultValue: CGFloat { 0 }
static func reduce(value: inout Value, nextValue: () -> Value) {
value = value + nextValue()
print("Reporting height: \(value)")
}
}

First, I used a PreferenceKey to pass the height from the "invisible" text view back up the view hierarchy. Then, I set the height of the TextEditor frame with that value.

Note that the view is now aligned to topLeading -- in your initial example, the invisible text was center aligned.

One thing I'm not crazy about is the use of the edge insets -- these feel like magic numbers (well, they are...) and I'd rather have a solution without them that still kept the Text and TextEditor completely aligned. But, this works for now.

Update, using UIViewRepresentable with UITextView

This seems to work and avoid the scrolling problems:


struct TaskView:View{
@Binding var text:String
@State private var textHeight : CGFloat = 40

var body: some View{
HStack(alignment:.top, spacing:8){
Button(action: {
print("Test")
}){
Circle()
.strokeBorder(Color.gray,lineWidth: 1)
.background(Circle().foregroundColor(Color.white))
.frame(width:22, height: 22)
}
.buttonStyle(PlainButtonStyle())

FieldView(text: $text, heightToTransmit: $textHeight)
.frame(height: textHeight)
.border(Color.red)

}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(EdgeInsets(top:10, leading:10, bottom: 10, trailing: 30))
.background(Color.white)
.cornerRadius(5)
}
}

struct FieldView : UIViewRepresentable {
@Binding var text : String
@Binding var heightToTransmit: CGFloat

func makeUIView(context: Context) -> UIView {
let view = UIView()
let textView = UITextView(frame: .zero, textContainer: nil)
textView.delegate = context.coordinator
textView.backgroundColor = .yellow // visual debugging
textView.isScrollEnabled = false // causes expanding height
context.coordinator.textView = textView
textView.text = text
view.addSubview(textView)

// Auto Layout
textView.translatesAutoresizingMaskIntoConstraints = false
let safeArea = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
textView.topAnchor.constraint(equalTo: safeArea.topAnchor),
textView.leadingAnchor.constraint(equalTo: safeArea.leadingAnchor),
textView.trailingAnchor.constraint(equalTo: safeArea.trailingAnchor)
])

return view
}

func updateUIView(_ view: UIView, context: Context) {
context.coordinator.heightBinding = $heightToTransmit
context.coordinator.textBinding = $text
DispatchQueue.main.async {
context.coordinator.runSizing()
}
}

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

class Coordinator : NSObject, UITextViewDelegate {
var textBinding : Binding<String>?
var heightBinding : Binding<CGFloat>?
var textView : UITextView?

func runSizing() {
guard let textView = textView else { return }
textView.sizeToFit()
self.textBinding?.wrappedValue = textView.text
self.heightBinding?.wrappedValue = textView.frame.size.height
}

func textViewDidChange(_ textView: UITextView) {
runSizing()
}
}
}

Updating a SwiftUI TextEditor on font change

Here is a sample

import SwiftUI
//Make your settings an ObservableObject
class MyUserSettings: ObservableObject{
@Published var fontSize: CGFloat = 48
}
struct MyFontView: View{
//Create the object in a parent view vs using a singleton
@StateObject var userSettings: MyUserSettings = MyUserSettings()
var body: some View{
NavigationView{
VStack{
//Mimic changing size
Slider(value: $userSettings.fontSize, in: 12...100, label: {
Text("Font Size = \(userSettings.fontSize)")
})
NavigationLink("editor view", destination: {
ChangingFontView()
})
}
}
//Inject it into the environment so you can access anywhere in the navigation view
.environmentObject(userSettings)
}
}
struct ChangingFontView: View {
//Access the environment object that will reload the view when there is a new value
@EnvironmentObject var userSettings: MyUserSettings
@State private var tvText: String = "tv Text"
var body: some View {
HStack()
{
TextEditor(text: $tvText)
//Use the environment variable
.font(.custom("AppleSDGothicNeo-SemiBold", size: CGFloat(userSettings.fontSize)))
.autocapitalization(.sentences)
.disableAutocorrection(true)
.border(Color.blue, width: 2)
//SwiftUI does not react well to harcoded values
//This is a key difference with UIKit
//Let SwiftUI do most of the worl
.frame(height: 96 + 8)
}
}
}

struct ChangingFontView_Previews: PreviewProvider {
static var previews: some View {
MyFontView()
}
}

I recommend you watch the SwiftUI videos from WWDC like Demystifying SwiftUI and all the other basic ones. It is different than UIKit nothing gets done without wrappers in SwiftUI these reload the view when there are updates.



Related Topics



Leave a reply



Submit