iosmacosswiftui

Mimicking behavior of iMessage with TextEditor for text entry


My first ever question here at Stack Overflow.

I'm writing a small application for iOS and macOS. Text entry is done via (now) a TextField and a Button. The issue with the TextField is that it's a single line and doesn't allow for multiline text entry. So, I tried using TextEditor instead, but I can either set it up to not grow as more text is added, or it shows up very big to begin with. What I'm saying is that ideally, it would mimic the behavior that the text entry in iMessage has: starts as the same size of a TextField but grows if needed to accommodate a multiline text like a TextEditor.

Here's the code I am using for this view:

var inputView: some View {
        HStack {
            ZStack {
                //tried this here... 
                //TextEditor(text: $taskText)
                TextField("New entry...", text: $taskText, onCommit: { didTapAddTask() })
                    .frame(maxHeight: 35)
                    .padding(EdgeInsets(top: 0, leading: 12, bottom: 0, trailing: 0))
                    .clipped()
                    .accentColor(.black)
                    .cornerRadius(8)
                    .textFieldStyle(RoundedBorderTextFieldStyle())
                Text(taskText).opacity(0).padding(.all, 8) 
            }
            
            Button(action: AddNewEntry, label: { Image(systemName: "plus.circle")
                    .imageScale(.large)
                    .foregroundColor(.primary)
                    .font(.title) }).padding(15).foregroundColor(.primary)
        }
    }

Any way of doing this? I tried different approaches found in different questions from other users, but I can't quite figure out.

Here's how it looks:

How the TextEdit looks

I also have tried something like this and played with different values for the .frame:

        TextEditor(text: $taskText)
            .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: 200)
            .border(Color.primary, width: 1)
            .padding(EdgeInsets(top: 0, leading: 12, bottom: 0, trailing: 0))

Any help is appreciated.

Thanks.

Oh, and I'm using Xcode 12.5.1 and target is iOS 14.x and macOS Big Sur for now.

EDIT to answer jnpdx. When I add the code from Dynamic TextEditor overlapping with other views this is how it looks, and it does not change dynamically.

How it looks when the app launches

How it looks when you type text


Solution

  • Here's an example, using your original code with the Button next to the TextEditor. The TextEditor grows until it hits the limit, defined by maxHeight. It also has a view for the messages (since you mentioned iMessage), but you could easily remove that.

    struct ContentView: View {
        @State private var textEditorHeight : CGFloat = 100
        @State private var text = "Testing text. Hit a few returns to see what happens"
        
        private var maxHeight : CGFloat = 250
        
        var body: some View {
            VStack {
                VStack {
                    Text("Messages")
                    Spacer()
                }
                Divider()
                HStack {
                    ZStack(alignment: .leading) {
                        Text(text)
                            .font(.system(.body))
                            .foregroundColor(.clear)
                            .padding(14)
                            .background(GeometryReader {
                                Color.clear.preference(key: ViewHeightKey.self,
                                                       value: $0.frame(in: .local).size.height)
                            })
                        
                        TextEditor(text: $text)
                            .font(.system(.body))
                            .padding(6)
                            .frame(height: min(textEditorHeight, maxHeight))
                            .background(Color.black)
                    }
                    .padding(20)
                    Button(action: {}) {
                        Image(systemName: "plus.circle")
                            .imageScale(.large)
                            .foregroundColor(.primary)
                            .font(.title)
                    }.padding(15).foregroundColor(.primary)
                }.onPreferenceChange(ViewHeightKey.self) { textEditorHeight = $0 }
            }
        }
    }
    
    
    struct ViewHeightKey: PreferenceKey {
        static var defaultValue: CGFloat { 0 }
        static func reduce(value: inout Value, nextValue: () -> Value) {
            value = value + nextValue()
        }
    }