SwiftUI TextField doesn't commit change when tapping another TextField - swift

I am building some basic form functionality in my app at the moment and I am having trouble with TextFields not changing the value in the relevant binded variable when tapping another TextField or pressing "Done" in Edit Mode.
#Binding var jobDetails: JobDetails
#Environment(\.colorScheme) var colorScheme: ColorScheme
...
var body: some View {
...
HStack {
Text("Hourly Rate")
Spacer()
TextField("", value: $jobDetails.hourlyRateBasic, formatter: TextFormatters().currencyFormatter())
.keyboardType(.asciiCapableNumberPad)
.multilineTextAlignment(.trailing)
...
In the iOS simulator, the field only seems to update when I physically hit the return key on my keyboard after typing in a new value (not the soft keyboard in the simulator). I would like the TextField to commit it's change to jobDetails.hourlyRateBasic when tapping another TextField or pressing "Done" to exit edit mode.
It seems that onEditingChanged fires when I tap another TextField, but I don't know how to leverage that into changing the jobDetails with the new value.

This is typical behavior of TextField in SwiftUI. Following is an example of it and alternative method to make TextField more responsive while typing.
import SwiftUI
struct ContentView: View {
#State private var text: String = "0"
#State private var num: Int = 0
private var resultString: String {
if let num = Int(self.text) {
return String(num*num)
}
return "0"
}
private var resultInt: Int {
return self.num*self.num
}
var body: some View {
VStack {
VStack(alignment:.leading) {
Text("Input number as String")
TextField("String Number",text: self.$text)
.textFieldStyle(RoundedBorderTextFieldStyle())
Text("Input number as Int")
TextField("Int Number", value: self.$num, formatter: NumberFormatter())
.textFieldStyle(RoundedBorderTextFieldStyle())
}
Spacer()
Text("From String")
Text("Square of \(self.text) is \(self.resultString)") .font(.title)
Spacer()
Text("From Int")
Text("Square of \(self.num) is \(self.resultInt)") .font(.title)
Spacer()
}.padding()
}
}

This is already fixed. Tested with Xcode 13.3 / iOS 15.4
struct TestView: View {
#State private var value1 = 1.0
#State private var text = ""
private var currencyFormatter: NumberFormatter = {
var nf = NumberFormatter()
nf.numberStyle = .currency
return nf
}()
var body: some View {
VStack {
HStack {
Text("Hourly Rate [\(value1)]")
Spacer()
TextField("", value: $value1, formatter: currencyFormatter)
.keyboardType(.asciiCapableNumberPad)
.multilineTextAlignment(.trailing)
}
HStack {
Text("Other")
Spacer()
TextField("Enter something", text: $text)
}
}
}
}

Related

How can I fix disabled text editor focusing bug in SwiftUI

I think there's a bug that disabled text editor is focussed when I get in and out into another tab. I want to totally disable text editors but I don't know how.
Seeing is believing.
struct TabViewWithTextEditor: View {
var body: some View {
TabView {
TextEditors()
.tabItem {
Image(systemName: "text.bubble")
Text("Text Editor")
}
AnotherView()
.tabItem {
Image(systemName: "shippingbox")
Text("Empty View")
}
}
}
}
struct TextEditors: View {
#State var textA: String = "Hello World"
#State var textB: String = "Placeholder"
#State var enabled: Bool = true
init() {
UITextView.appearance().backgroundColor = .clear // To apply background color.
}
var body: some View {
VStack {
Text("Text Editor")
TextEditor(text: $textA)
.background(enabled ? .gray : .red)
.foregroundColor(.black)
.disabled(!enabled)
TextEditor(text: $textB)
.background(enabled ? .yellow : .red)
.foregroundColor(.black)
.disabled(!enabled)
Toggle("Enable Text Editors", isOn: $enabled)
}
.padding(30)
}
}
struct AnotherView: View {
var body: some View {
Text("Empty View")
}
}
And it looks like
It looks like the issue is due to TextEditor (or UITextView at backend) preserves focus, probably due to a bug.
Here is safe workaround - remove focus forcefully before disable
Tested with Xcode 13.4 / iOS 15.5
#FocusState private var focused: Bool
var isEditing: Binding<Bool> {
Binding(get: { enabled } , set: {
if !$0 {
focused = false // << here !!
}
enabled = $0
})
}
// ...
Toggle("Enable Text Editors", isOn: isEditing)

Remove padding on TextEditor

My custom text editor below once you click on the pen to edit, a new space appears so the text from before is not on the same line as the new one. How can I fix this? Here's a simple reproducible example:
struct SwiftUIView: View {
#State var name: String = "test"
#State var showEdit: Bool = true
var body: some View {
HStack {
HStack {
if(showEdit) {
CustomTextEditor.init(placeholder: "My unique name", text: $name)
.font(.headline)
} else {
Text(name)
.font(.headline)
}
}
Spacer()
Button(action: {
showEdit.toggle()
}) {
Image(systemName: "pencil")
.foregroundColor(.secondary)
}
}
}
}
struct CustomTextEditor: View {
let placeholder: String
#Binding var text: String
var body: some View {
ZStack {
if text.isEmpty {
Text(placeholder)
.foregroundColor(Color.primary.opacity(0.25))
}
TextEditor(text: $text)
}.onAppear() {
UITextView.appearance().backgroundColor = .clear
}.onDisappear() {
UITextView.appearance().backgroundColor = nil
}
}
}
I want it to have the same padding properies as inserting a simple Text("") so when I switch between Text("xyz") and TextEditor(text: $xyz) it has the same padding alignment. Right now TextEditor has a weird padding.
You will drive yourself insane trying to line up a Text and a TextEditor (or a TextField, for that matter), so don't try. Use another, disabled, TextEditor instead, and control the .opacity() on the top one depending upon whether the bound variable is empty or not. Like this:
struct CustomTextEditor: View {
#Binding var text: String
#State private var placeholder: String
init(placeholder: String, text: Binding<String>) {
_text = text
_placeholder = State(initialValue: placeholder)
}
var body: some View {
ZStack {
TextEditor(text: $placeholder)
.disabled(true)
TextEditor(text: $text)
.opacity(text == "" ? 0.7 : 1)
}
}
}
This view will show the placeholder if there is no text, and hide the placeholder as soon as there is text.
Edit:
You don't need the button, etc. in your other view. It becomes simply:
struct SwiftUIView: View {
#State var name: String = ""
var body: some View {
CustomTextEditor.init(placeholder: "My unique name", text: $name)
.font(.headline)
.padding()
}
}
and if you need a "Done" button on the keyboard, change your CustomTextEditor() to this:
struct CustomTextEditor: View {
#Binding var text: String
#State private var placeholder: String
#FocusState var isFocused: Bool
init(placeholder: String, text: Binding<String>) {
_text = text
_placeholder = State(initialValue: placeholder)
}
var body: some View {
ZStack {
TextEditor(text: $placeholder)
.disabled(true)
TextEditor(text: $text)
.opacity(text == "" ? 0.7 : 1)
.focused($isFocused)
}
.toolbar {
ToolbarItemGroup(placement: .keyboard) {
Button {
isFocused = false
} label: {
Text("Done")
.foregroundColor(.accentColor)
.padding(.trailing)
}
}
}
}
}

Pin TextField to Keyboard in SwiftUI

Is it possible to pin a textfield to the top of the keyboard in SwiftUI? Pretty much identical to the messaging app on iOS where it it is at the bottom of the screen on appear, but moves with the keyboard when you click it, but with SwiftUI, I've looked around and not been able to find anything.
You can achieve this with a toolbar modifier and an toolbaritemgroup with .keyboard placement.
struct ToolBarTest: View {
#State private var text: String = ""
#FocusState private var focus: Bool
#FocusState private var focusToolbar: Bool
var body: some View {
ZStack {
VStack{
Spacer()
TextField("toolbar", text: $text)
.textFieldStyle(.roundedBorder)
.opacity(focus || focusToolbar ? 0 : 1)
.focused($focus)
}
.toolbar {
ToolbarItemGroup(placement: .keyboard){
TextField("toolbar", text: $text)
.textFieldStyle(.roundedBorder)
.focused($focusToolbar)
}
}
.onChange(of: focus) {
if $0{
focusToolbar = true
}
}
}
}
}

How can I remove TextField focus when tapping a different view using .focused()?

I'm trying to use iOS15's .focused() modifier to enable the user to tap anywhere outside of a text field to remove focus. I am going off of the example provided in: https://www.youtube.com/watch?v=GqXVFXnLVH4. Below is my non-working attempt:
enum Field {
case textField
case notTextField
}
struct ContentView: View {
#State var textInput = ""
#FocusState var focusState:Field?
var body: some View {
VStack {
TextField("Enter some text...", text: $textInput)
.focused($focusState, equals: .textField)
Rectangle()
.focused($focusState, equals: .notTextField)
.onTapGesture {
state = .notTextField
}
}
}
}
I found the solution was not to focus another element, but to switch the textfield's focus to false:
#State var textInput = ""
#FocusState var textFieldIsFocused: Bool
var body: some View {
VStack {
TextField("Enter some text...", text: $textInput)
.focused($textFieldIsFocused)
Rectangle()
.onTapGesture {
textFieldIsFocused = false
}
}
}

TextField with animation crashes app and looses focus

Minimal reproducible example:
In SceneDelegate.swift:
let contentView = Container()
In ContentView.swift:
struct SwiftUIView: View {
#State var text: String = ""
var body: some View {
VStack {
CustomTextFieldView(text: $text)
}
}
}
struct Container: View {
#State var bool: Bool = false
#State var text: String = ""
var body: some View {
Button(action: {
self.bool.toggle()
}) {
Text("Sheet!")
}
.sheet(isPresented: $bool) {
SwiftUIView()
}
}
}
In CustomTextField.swift:
struct CustomTextFieldView: View {
#Binding var text: String
#State var editing: Bool = false
var body: some View {
Group {
if self.editing {
textField
.background(Color.red)
} else {
ZStack(alignment: .leading) {
textField
.background(Color.green)
Text("Placeholder")
}
}
}
.onTapGesture {
withAnimation {
self.editing = true
}
}
}
var textField: some View {
TextField("", text: $text)
}
Problem:
After running the above code and focusing the text field, the app crashes. Some things I noticed:
If I remove the withAnimation code, or the ZStack in CustomTextField file, the app doesn't crash, but the TextField looses focus.
If I remove the VStack in SwiftUIView, the app doesn't crash, but the TextField looses focus.
If I use a NavigationLink or present the TextField without a sheet, the app doesn't crash, but the TextField looses focus.
Questions:
Is this a problem in the current version of SwiftUI?
Is there a solution to this problem using SwiftUI? I want to stay out of
ViewRepresentables as much as possible.
How can I keep the focus of the TextField after the body is recalculated because of a change in state?
How can I keep the focus of the TextField after the body is
recalculated because of a change in state?
You have two of them. Two different TextField could not be in editing state at the same time.
The approach suggested by Asperi is the only possible.
The reason, why your code crash is not easy explain, but expected in current SwiftUI.
You have to understand, that Group is not a standard container, it just like a "block" on which you can apply some modifiers. Removing Group and using wraping body in ViewBuilder
struct CustomTextFieldView: View {
#Binding var text: String
#State var editing: Bool = false
#ViewBuilder
var body: some View {
if self.editing {
TextField("", text: $text)
.background(Color.red)
.onTapGesture {
self.editing.toggle()
}
} else {
ZStack(alignment: .leading) {
TextField("", text: $text)
.background(Color.green)
.onTapGesture {
self.editing.toggle()
}
}
}
}
}
the code will stop to crash, but there is other issue, the keyboard will dismiss immediately. That is due the tap gesture applied.
So, believe or not, you have to use ONE TextField ONLY.
struct CustomTextFieldView: View {
#Binding var text: String
#State var editing = false
var textField: some View {
TextField("", text: $text, onEditingChanged: { edit in
self.editing = edit
})
}
var body: some View {
textField.background(editing ? Color.green : Color.red)
}
}
Use this custom text field elsewhere in your code, as you want
Try the following for CustomTextFieldView (tested & works with Xcode 11.2 / iOS 13.2)
struct CustomTextFieldView: View {
#Binding var text: String
#State var editing: Bool = false
var body: some View {
TextField("", text: $text)
.background(self.editing ? Color.red : Color.green)
.onTapGesture {
withAnimation {
self.editing = true
}
}
}
}
How can I keep the focus of the TextField after the body is recalculated because of a change in state?
You don't loose focus, you just remove entire text field, so the solution is not replace text field, but modify its property, ie background. It's ok to put it into ZStack, but keep it one.