Alpheratz
Alpheratz

Reputation: 39

SwiftUI vertical axis TextFields collapses to nothing when .fixedSize() is applied

iOS 16 (finally) allowed us to specify an axis: in TextField, letting text entry span over multiple lines.

However, I don't want my text field to always fill the available horizontal space. It should fill the amount of space taken up by the text that has been entered into it. To do this, we can apply .fixedSize().

However, using this two things in conjunction causes the text field to completely collapse and take up no space. This bug (?) does not affect a horizontal-scrolling text field.

Is this basic behaviour simply broken, or is there an obtuse but valid reason these methods don't play nice?

This is very simple to replicate:

struct ContentView: View {
    @State var enteredText: String = "Test Text"
    
    var body: some View {
        TextField("Testing", text: $enteredText, axis: .vertical)
            .padding()
            .fixedSize()
            .border(.red)
    }
}

Running this will produce a red box the size of your padding. No text is shown.

Upvotes: 2

Views: 756

Answers (4)

user28447657
user28447657

Reputation: 1

I solved the problem by modifying the TextField with

.lineLimit(2, reservesSpace: true) 

Upvotes: 0

M Wilm
M Wilm

Reputation: 304

On macOS the situation seems to be a bit more complicated. The problem seems to be that TextField does not extent its rendering surface beyond its initial size. So - when the field grows the text is invisible because not rendered.

I am using the following ViewModifier to force a larger rendering surface. I fear this can be called a "hack":

// For a scrollable TextField
struct DynamicMultiLineTextField: ViewModifier {
      
   let minWidth: CGFloat
   let maxWidth: CGFloat
   let font: NSFont
   @Binding var text: String
   
   @FocusState var isFocused: Bool
   @State var firstActivation : Bool = true
    
   @State var backgroundFieldSize: CGSize? = nil
   var fieldSize : CGSize {
      get {
         if let theSize = backgroundFieldSize {
            return theSize
         }
         else {
            return self.sizeOfText()
         }
      }
   }
   
   
   func sizeOfText() -> CGSize {
      let font = self.font
      let stringValue = self.text
      let attributedString = NSAttributedString(string: stringValue, attributes: [.font: font])
      let mySize = attributedString.size()
      let theSize = CGSize(width: min(self.maxWidth, max(self.minWidth, mySize.width + 5)), height: mySize.height)
      return theSize
   }
      
      func body(content: Content) -> some View {
         content
            .frame(width:self.fieldSize.width)
            .focused(self.$isFocused)
            .onChange(of: self.isFocused, perform: { value in
               if value && self.firstActivation {
                  let oldText = self.text
                 
                  self.backgroundFieldSize = CGSize(width:self.maxWidth, height:self.sizeOfText().height)
                  Task() {@MainActor () -> Void in
                     self.text = "nonsense text nonsense text nonsense text nonsense text nonsense text nonsense text nonsense text nonsense text"
                     self.firstActivation = false
                     self.isFocused = false
                  }
                  
                  Task() {@MainActor () -> Void in
                     self.text = oldText
                     try? await Task.sleep(nanoseconds: 1_000)
                     self.isFocused = true
                     self.backgroundFieldSize = nil
                     Task () {
                        self.firstActivation = true
                     }
                  }
               }
            })
      }
   }

extension View {
   func scrollableDynamicWidth(minWidth: CGFloat, maxWidth: CGFloat, font: NSFont, text: Binding<String>) -> some View {
      self.modifier(DynamicMultiLineTextField(minWidth: minWidth, maxWidth: maxWidth, font: font, text:text))
   }
}

Usage:

TextField("Content", text:self.$tableCell.value, axis:.vertical)
         .scrollableDynamicWidth(minWidth: 100, maxWidth: 800, font: self.tableCellFont, text: self.$tableCell.value)

Upvotes: 0

M Wilm
M Wilm

Reputation: 304

I had exactly the same problem with multiline text. So far the use of axis:.vertical requires a fixed width for the text field. This was for me a major problem when designing a table view where the column width adapts to the widest text field. I found a very good working solution which I summarised in the following ViewModifier :

struct DynamicMultiLineTextField: ViewModifier {
      
      let minWidth: CGFloat
      let maxWidth: CGFloat
      let font: UIFont
      let text: String
      
      var sizeOfText : CGSize {
         get {
            let font = self.font
            let stringValue = self.text
            let attributedString = NSAttributedString(string: stringValue, attributes: [.font: font])
            let mySize = attributedString.size()
            return CGSize(width: min(self.maxWidth, max(self.minWidth, mySize.width)), height: mySize.height)
         }
      }
      
      func body(content: Content) -> some View {
         content
            .frame(minWidth: self.minWidth, idealWidth: self.sizeOfText.width ,maxWidth: self.maxWidth)
      }
   }

extension View {
   func scrollableDynamicWidth(minWidth: CGFloat, maxWidth: CGFloat, font: UIFont, text: String) -> some View {
      self.modifier(DynamicMultiLineTextField(minWidth: minWidth, maxWidth: maxWidth, font: font, text: text))
   }

Usage (only on a TextField with the option: axis:.vertical):

TextField("Content", text:self.$tableCell.value, axis: .vertical)
               .scrollableDynamicWidth(minWidth: 100, maxWidth: 800, font: self.tableCellFont, text: self.tableCell.value)

The text field width changes as you type. If you want to limit the length of a line type "option-return" which starts a new line.

Upvotes: 0

storoj
storoj

Reputation: 1867

I don't want my text field to always fill the available horizontal space. It should fill the amount of space taken up by the text that has been entered into it.

That's a weird wish. If you want to remove the background of the TextField, then do it. But I don't think it's a good idea to have an autosizing TextField. One of the reasons against it is the fact that if you erase all the text then the TextField will collapse to the zero width and you'll never set the cursor into it.

Upvotes: -1

Related Questions