SwiftUI Form Layout Bug with Animation - forms

I have read similar descriptions about this issue but none are exactly this and the work-arounds do not seem applicable to me. I would like to know if anyone else has experienced this, if there is a way to use animation to display a Form, or if this is worth reporting as a bug to Apple.
I have ContentView with a couple of buttons which toggle a boolean. One button simply toggles the boolean while the other toggles the boolean within an animation block. The boolean controls which of two Form views are displayed. When toggled without the animation the Forms render correctly. But when toggled within the animation block, the Forms are rendered incorrectly.
This is the "Normal" (aka ReviewSheet) view.
When you tap "Edit" it switches to the BugSheet view. Looks fine.
If you pick the "Bug It" button this is how BugSheet is rendered:
The Form in BugSheet is now rendered with the first section's header overlapping an enlarged content area and in the second section, the Text is no longer rendered onto multiple lines (using lineLimit modifier has no effect).
Here is the code for each of these three views:
struct ContentView: View {
#State private var isEditing = false
var body: some View {
VStack(alignment: .leading, spacing: 0) {
HStack {
Text("Form Bug")
.font(.title)
Spacer()
// This "Edit" button just toggles isEditing and doing this
// the switch between BugSheet and ReviewSheet appears as you
// would expect.
Button(isEditing ? "Save 1" : "Edit") {
isEditing.toggle()
}
// This "Edit" button also toggle isEditing but does so within
// an animation block. The switch between BugSheet and ReviewSheet
// leaves the Form rendered improperly.
Button(isEditing ? "Save 2" :
"Bug It") {
withAnimation {
isEditing.toggle()
}
}
}
.padding()
.background(Color.orange)
.foregroundColor(.white)
if isEditing {
BugSheet()
} else {
ReviewSheet()
}
}
}
}
struct ReviewSheet: View {
#State private var titleText: String = "Example Title"
#State private var shortDescription: String = "This should work"
#State private var keywords: String = "swiftui, apple, form, bug"
let longDescription = "The rain in Spain stays mainly on the Plains even while the quick brown fox jumps over the lazy dog."
var body: some View {
Form {
Section(header: Text("Title")) {
Text(titleText)
}
Section(header: Text("Details")) {
Text(shortDescription)
Text(longDescription)
Text(keywords)
}
}
}
}
struct BugSheet: View {
enum Field: Hashable {
case title
case shorty
case keywords
}
#FocusState private var focusedField: Field?
#State private var titleText: String = ""
#State private var shortDescription: String = ""
#State private var keywords: String = ""
#State private var notes: String = "This is a starter note."
let longDescription = "The rain in Spain stays mainly on the Plains even while the quick brown fox jumps over the lazy dog."
private func gotoField(_ field: Field) {
focusedField = field
}
var body: some View {
Form {
Section(header: Text("Title")) {
TextField("title here", text: $titleText, onCommit: {gotoField(.shorty)})
.autocapitalization(.words)
.disableAutocorrection(false)
.keyboardType(.default)
.focused($focusedField, equals: .title)
}
Section(header: Text("Details")) {
TextField("short description", text: $shortDescription, onCommit: {gotoField(.keywords)})
.autocapitalization(.sentences)
.disableAutocorrection(false)
.keyboardType(.default)
.focused($focusedField, equals: .shorty)
Text(longDescription)
TextField("keywords", text: $keywords, onCommit: {gotoField(.title)})
.autocapitalization(.none)
.disableAutocorrection(true)
.keyboardType(.default)
.focused($focusedField, equals: .keywords)
}
}
}
}

Related

How do I remove focus from a TextField in SwiftUI on MacOS?

I have a view in which the user enters data into a several textfields, but I can't allow the user to exit the textfields; one of them is always selected. How do I make the fields un-focus when I click on something else (the background, the submit button, etc.)?
Current View:
struct ContentView: View {
var body: some View {
Form {
TextField("Username", text: $username)
TextField("Password", text: $password)
Button("Submit") {
// Submit data
}
}
}
}
You need to use an optional #FocusState and use .allowsHitTesting(true). On the form, you put a .onTapGesture that sets #FocusState to nil.
struct ContentView: View {
#State private var username: String = ""
#State private var password: String = ""
#FocusState private var focusedField: String?
var body: some View {
Form {
TextField("Username", text: $username)
.focused($focusedField, equals: "user")
TextField("Password", text: $password)
.focused($focusedField, equals: "password")
Button("Submit") {
// Submit data
focusedField = nil
}
}
.onTapGesture {
focusedField = nil
}
}
}
The .allowsHitTesting(true) lets the TextFields accept tap gestures directly, otherwise they would be blocked by the Form's .onTapGesture.
On macOS, in the sidebar, the form is only the size of the Button and TextFields. If you wanted a larger tap area, you would need some kind of background to place it on.

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.

Swiftui: how to watch variable binded to text field always

I am using Swift-UI for creating my app.
There is an AccountView is listing user's attributes and you can update it.
Once you click an Update button on the user's variable row of the list, navigate to EditVariableView, where you can change the variable with Text Field.
Of course, the text field has a validation of the inputted text, and you can commit the change by the Submit button on the right-up corner of EditVariableView.
For validation of the input, I use onCommit, detecting the change of the input, but here is a problem.
When you touch the text field, the keyboard comes out, and also you can input the text. But onCommit emits an event only when you close the keyboard.
If you input the text and click the Submit button without closing the keyboard, certainly onCommit does not emit an event for the validation. So, of course, the validation won't be done.
I want you to tell me, how to detect the input change on every text change.
You can disable Submit button if TextField is in editing state
import SwiftUI
struct ContentView: View {
#State var txt: String = ""
#State var editingFlag = false
var body: some View {
VStack {
TextField("text", text: $txt, onEditingChanged: { (editing) in
self.editingFlag = editing
}) {
print("commit")
}.padding().border(Color.red)
Button(action: {
print("submit")
}) {
Text("SUBMIT")
}.disabled(editingFlag)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
SwiftUI 2.0
With the Xcode 12 and from iOS 14, macOS 11, or any other OS contains SwiftUI 2.0, there is a new modifier called 'onChange' that detects any change of the given state and can be performed on any view. So with some minor refactoring:
struct ContentView: View {
#State var txt: String = ""
#State var editingFlag = false
var body: some View {
VStack {
TextField("text", text: $txt)
.onChange(of: txt) {
print("Changed to :\($0)")
}
}
.padding()
.border(Color.red)
Button("SUBMIT") {
print("submit")
}
.disabled(editingFlag)
}
}
from: hacking with with Swift. Paul Hudson
struct ContentView : View {
#State private var name = ""
var body: some View {
TextField("Enter your name:", text: $name)
.textFieldStyle(RoundedBorderTextFieldStyle())
.onChange(of: name) { newValue in
print("Name changed to \(name)!")
}
}
}