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.
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
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:
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:
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
How to Stream Remote Audio in iOS 13? (Swiftui)
How to Set an Environment Object in Preview
Swift: Providing a Default Protocol Implementation in a Protocol Extension
Physics Detecting Collision Multiple Times
How to Debug "Precondition Failure" in Xcode
Declaring and Using Custom Attributes in Swift
Missing Argument for Parameter 'From' in Call When Creating Instance of Codable Class
Print the Nstableview's Row Number of the Row Clicked by the User
Swift Utf8 Encoding and Non Utf8 Character
Cannot Invoke 'Filter' with an Argument List of Type '((_) -> _)'