Reputation: 39
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
Reputation: 1
I solved the problem by modifying the TextField
with
.lineLimit(2, reservesSpace: true)
Upvotes: 0
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
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
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