Dynamic height for TextEditor

I'm trying to fit the TextEditor inside a ScrollView.
Is there a way to make TextEditor only takes up the space that it needs to fit all text?
or simply, how to change the height of the TextEditor dynamically to fit all the text?

Replies

Haven't tried it myself, but I would give the TextView a .fixedSize(horizontal: false, vertical: true). Works for regular ScrollViews pretty well.

Edit: I tried it. Doesn't seem to work. I'm annoyed. I think you'll need to dip into UIKit.
This solution is adapted from responses (primarily Mojtaba Hosseini) to a couple of similar questions on StackOverflow and also from Alan Quatermain's well written blog titled "SwiftUI Bindings with CoreData".

The cross platform appearance is not identical and the need to add the hacks is annoying, but it works.

I use this solution for both macOS and iOS targets (with Core Data) and an @ObservedObject var. I've adapted this answer to work independently with an @State var. This builds and runs in both targets but some of the view characteristics are "out-of-sync".

Essentially TextEditor and Text views must be drawn in the same way - must have identical view modifier characteristics, e.g. .font modifier, etc. The Text view "ghosts" the TextEditor and performs two roles:
  1. presents the placeholder text when the Optional<String> (@State private var objectDescription: String?) is nil.

  2. dynamically expands the height of the stacked views to accommodate multiple lines of text in the TextEditor.

If anyone understands the mechanics behind what makes the "ghosting" technique work in the ZStack, please add an answer, because I'm still trying to figure that out.


Code Block
struct TextEditorView: View {
   @State private var objectDescription: String?
  var body: some View {
        VStack(alignment: .leading) {
            let placeholder = "enter detailed Description"
            Text("Description")
            ZStack(alignment: .topLeading) {
                TextEditor(text: Binding($objectDescription, replacingNilWith: ""))
                    .frame(minHeight: 30, alignment: .leading)
                    // following line is a hack to force TextEditor to appear
                    //  similar to .textFieldStyle(RoundedBorderTextFieldStyle())...
                    .cornerRadius(6.0)
                    .foregroundColor(Color(.labelColor))
                    .multilineTextAlignment(.leading)
                Text(objectDescription ?? placeholder)
                    // following line is a hack to create an inset similar to the TextEditor inset...
                    .padding(.leading, 4)
                    .foregroundColor(Color(.placeholderTextColor))
                    .opacity(objectDescription == nil ? 1 : 0)
            }
            .font(.body) // if you'd prefer the font to appear the same for both iOS and macOS
        }
    }
}


// Full credit for this extension to Binding to Alan Quatermain.

Code Block
public extension Binding where Value: Equatable {
    init(_ source: Binding<Value?>, replacingNilWith nilProxy: Value) {
        self.init(
            get: { source.wrappedValue ?? nilProxy },
            set: { newValue in
                if newValue == nilProxy {
                    source.wrappedValue = nil
                }
                else {
                    source.wrappedValue = newValue
                }
        })
    }

Should also mention that if you choose to adopt my answer as a solution and want it to build on both platforms, you'll need (as at Xcode 12.0 beta 3) an extension to Color...

Code Block
extension Color {
    static var foregroundLabel: Color {
        #if os(macOS)
        return Color(.labelColor)
        #else
        return Color(.label)
        #endif
    }
    static var placeholderText: Color {
        #if os(macOS)
        return Color(.placeholderTextColor)
        #else
        return Color(.placeholderText)
        #endif
    }
}

Then use in code as...

Code Block
                TextEditor(text: Binding($objectDescription, replacingNilWith: ""))
                    ...
                    .foregroundColor(Color.foregroundLabel)
                    ...
                Text(objectDescription ?? placeholder)
                    ...
                    .foregroundColor(Color.placeholderText)
                    ...


Actually for me worked wrapping the TextEditor in any view with a corner radius... This is a very strange workaround.