skip to Main Content

I want to make a TextField in SwiftUI that adjusts its width dynamically based on the content, without adding extra spacing.

The TextField is part of an HStack with several Text views, and my goal is to have it appear seamlessly as part of the text. For example, the result should look something like this:

Text("Text(") + Text(""") + TextField + Text(""") + Text(")")

I tried using the .fixedSize() modifier, which works fine when the text becomes longer. However, it does not shrink the width when the text gets shorter, the TextField retains the initial size.

Here’s a simplified example of what I’ve tried:

import SwiftUI

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

    var body: some View {
        HStack(spacing: 0) {
            Text("Text(")
            Text(""")
            TextField("Placeholder", text: $text)
                .fixedSize()
                .textFieldStyle(PlainTextFieldStyle())
            Text(""")
            Text(")")
        }
        .font(.system(size: 15, weight: .medium, design: .monospaced))
    }
}

The TextField works fine for longer text, but it doesn’t shrink dynamically when the content becomes shorter than initial one.

Is there a way to have the TextField behave like the Text views, adjusting its width dynamically without leaving extra spacing? Or is manual width calculation the only option?

Any help would be greatly appreciated!

2

Answers


  1. You might experiment with the fixedSize modifier:

    HStack {
        TextField("Prefix", text: $prefix)
            .fixedSize()
        TextField("Text", text: $text)
            .fixedSize()
        TextField("Suffix", text: $suffix)
            .fixedSize()
    }
    

    Possibly combined with constraining the min width:

    HStack {
        TextField("Prefix", text: $prefix)
            .frame(minWidth: 10, maxWidth: 300)
            .fixedSize()
        TextField("Text", text: $text)
            .fixedSize()
        TextField("Suffix", text: $suffix)
            .fixedSize()
    }
    

    Note that the TextField’s width will not shrink smaller than its prompt/placeholder would take.

    Update:

    This simple solution does not always work correctly: when text has been entered, then this text has been selected, and then deleted, the text field won’t shrink again. It seems to work, when deleting single characters, though. So, we need to investigate this further.

    Login or Signup to reply.
  2. You could show the same text in the background and measure its width using .onGeometryChange. Then apply this width as fixed width to the TextField.

    • Although not apparent from its name, .onGeometryChange also reports the initial size.
    • Apply .fixedSize() to the background version, to allow it to exceed the bounds of its frame.
    • Also apply .hidden(), to prevent it from being seen.
    @State private var textWidth = CGFloat.zero
    
    TextField("Placeholder", text: $text)
        .frame(width: textWidth)
        .textFieldStyle(PlainTextFieldStyle())
        .background {
            Text(text.isEmpty ? "Placeholder" : text)
                .fixedSize()
                .hidden()
                .onGeometryChange(for: CGFloat.self) { proxy in
                    proxy.size.width
                } action: { newVal in
                    textWidth = newVal
                }
        }
    

    Animation

    You might like to wrap the size-reading part as a ViewModifier, to make it more re-usable. The answer to How to get size of child? shows how this can be done (it was my answer).

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search