I have multiple TextFields and for one of them I need to switch the keyboardType to .numberPad. Obviously this is really easy using the .keyboardType(.numberPad) modifier, but the issue is that when the default keyboard is already shown it doesn't dismiss and then bring up the numberPad. Instead, it just switches immediately which causes the user to have to manually swipe up to see the TextField in which they will be entering numbers.
What I need to do is completely dismiss the default keyboard and then show the numberPad so that the correct TextField is pushed up on screen.
I've tried various modifiers for the numberField TextField such as:
.onAppear {
isFocused = nil
isFocused = .numberField
}
.onAppear {
isFocused = nil
sleep(1)
isFocused = .numberField
}
.focused($isFocused, equals: nil)
.keyboardType(.numberPad)
.focused($isFocused, equals: .numberField)
None work. The keyboard switches, but the screen does not scroll to the correct field.
As this is a multi-field form, I am using the submit button to switch between fields like this:
TextField("", text: $field1)
.focused($isFocused, equals: .field1)
.keyboardType(.default)
.onSubmit {
if isFocused == .field1 {
isFocused = .numberField
}
}
.submitLabel(.next)
TextField("", text: $numberField)
.focused($isFocused, equals: .numberField)
.keyboardType(.numberPad)
If I manually dismiss the default keyboard for field1 and then tap on the numberField TextField, the numberPad comes up and the field focuses correctly. So, I need to somehow automatically dismiss the default keyboard and then show the numberPad so the correct field is focused on screen for the user.
In order to dismiss an existing keyboard and then show a new type of keyboard, while maintaining keyboard avoidance and focusing on the correct TextField, you need to use DispatchQueue as follows:
TextField("", text: $field1)
.focused($isFocused, equals: .field1)
.keyboardType(.default)
.onSubmit {
if isFocused == .field1 {
isFocused = nil
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
isFocused = .numberField
}
}
}
By using DispatchQueue you allow the app to keep running, while completely dismissing the keyboard, waiting x seconds (in my example it's 0.3 seconds which feels pretty instantaneous) and then bringing up the new keyboard.
Related
I'm building a SwiftUI to-do app. You tap an Add button that pulls up a partial-height sheet where you can enter and save a new to-do. The Add sheet's input (TextField) should be focused when the sheet appears, so in order to keep things feeling fast and smooth, I'd like the sheet and the keyboard to animate onscreen together, at the same time. After much experimentation and Googling, I still can't figure out how to do it.
It seems like there are two paths to doing something like this:
(1) Autofocus the sheet
I can use #FocusState and .onAppear or .task inside the sheet to ensure the TextField is focused as soon as it comes up. It's straightforward functionally, but I can't find a permutation of it that will give me that single animation: it's sheet, then keyboard, presumably because those modifiers don't fire until the sheet is onscreen.
(2) Keyboard accessory view / toolbar
The .toolbar modifier seems tailor-made for a view of custom height that sticks to the keyboard--you lose the nice sheet animation but you gain the ability to have the view auto-size. However, .toolbar is designed to present controls alongside a TextField that itself isn't stuck to the keyboard. That is, the field has to be onscreen before the keyboard so it can receive focus...I don't know of a way to put the input itself inside the toolbar. Seems like chat apps have found a way to do this but I don't know what it is.
Any help would be much appreciated! Thanks!
Regarding option (1), I think there is no way to sync the animation. I decided to do it this way and don't worry about the delay between sheet and keyboard animation. Regarding option (2), you could try something like this:
struct ContentView: View {
#State var text = ""
#FocusState var isFocused: Bool
#FocusState var isFocusedInToolbar: Bool
var body: some View {
Button("Show Keyboard") {
isFocused = true
}
.opacity(isFocusedInToolbar ? 0 : 1)
TextField("Enter Text", text: $text) // Invisible Proxy TextField
.focused($isFocused)
.opacity(0)
.toolbar {
ToolbarItem(placement: .keyboard) {
HStack {
TextField("", text: $text) // Toolbar TextField
.textFieldStyle(.roundedBorder)
.focused($isFocusedInToolbar)
Button("Done") {
isFocused = false
isFocusedInToolbar = false
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
}
}
}
.onChange(of: isFocused) { newValue in
if newValue {
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.05) {
isFocusedInToolbar = true
}
}
}
}
}
The trick is, that you need a TextField in your content that triggers the keyboard initally and then switch focus to the TextField in the toolbar. Otherwise you won't get the keyboard to show up.
Automatic keyboard avoidance seems to work fine if it's a regular TextField (i.e. one that doesn't expand on an axis), whether or not it is contained in a ScrollView
Keyboard avoidance also seems to work with the new TextField(_:text:axis) introduced in iOS 16 if it's simply placed in a VStack without being wrapped in a ScrollView. It will even continue to avoid the keyboard correctly as the height expands with more text.
But I can't seem to get keyboard avoidance to work with TextField(_:text:axis) if it is placed inside a ScrollView
I can employ the hacky method of using a ScrollViewReader combined with DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(400)) to wrap the proxy.scrollTo() when the TextField is focused. This sort of works when you first focus the field, but I can't seem to get the ScrollView to continue to adjust its position as the TextField expands.
Here is an example:
struct KeyboardAvoidingView: View {
#State var text = ""
var body: some View {
ScrollViewReader { proxy in
ScrollView {
VStack {
Color.red
.frame(height: 400)
Color.blue
.frame(height: 400)
TextField("Name", text: $text, axis: .vertical)
.padding(.vertical)
.onTapGesture {
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(400)) {
withAnimation(.default) {
proxy.scrollTo(0)
}
}
}
.onChange(of: text) { newValue in
proxy.scrollTo(0) // This doesn't seem to do anything
}
Spacer()
.frame(height: 0)
.id(0)
}
}
}
}
}
I guess I'm wondering whether this is expected behavior, or a bug. And regardless if it's one or the other, I'm wondering if I can have an auto-expanding text field inside a scroll view that I can make avoid the keyboard even as the height of the field expands?
UPDATE: It turns out, the issue was with placing the TextField inside a VStack instead of a LazyVStack. I assume ScrollView doesn't know what to do with just a regular VStack in certain situations. If I replace the VStack with a LazyVStack in my example, everything works as expected!
I answered the question with the update posted above. The issue was with using VStack instead of LazyVStack
This is a long time known bug in the TextField component, but you may achieve the desired behavior by using an anchor: .bottom in the proxy.scrollTo call of your onChange.
it'll look like this:
// ...
TextField("Name", text: $text, axis: .vertical)
.padding(.vertical)
.onTapGesture {
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(400)) {
withAnimation(.default) {
proxy.scrollTo(0)
}
}
}
.onChange(of: text) { newValue in
// This will always scroll to the bottom of the text editor,
// just make sure to pass the right value in the first parameter
// that will identify your TextEditor
proxy.scrollTo(MyTextEditorId, anchor: .bottom)
}
// ...
You may need some additional work to handle the editing of upper parts of the text editor when it's taller than your screen
I wanna give my textfield some extra space while SwiftUI is automatically scrolling to it. As described in this question: How to add more padding below a TextField. With the answer from this one it works fine: https://developer.apple.com/forums/thread/699111 but cause I am manipulating the safe area of the view my button touches are not accepted anymore if they are outside of the safe area.
ScrollView {
VStack {
Spacer(minLength: 1000)
TextField("Textfield 1", text: $text)
.padding(30)
Button {
print("Button tapped")
} label: {
Text("Click this button")
}
Spacer(minLength: 1000)
}
}
.keyboardAvoiding()
This example code shows the problem. If I am selecting the TextField and the keyboard appears there's enough space below but If I am clicking at the button nothing happens. Any idea to work around this behaviour or any other way to add more padding below the TextField in SwiftUI?
I'm trying to build a chat view in SwiftUI and I want to append my input views to the keyboard, so that when I dismiss the keyboard by dragging my view gets moved with it.
When I was using UIKit I overwrote the inputAccessoryView of the ViewController. Is something similar possible with SwiftUI?
EDIT:
I already saw that I can add a UIKit TextField and add a InputAccessory for this text field. However that's not what I want to do. I want to have a global inputAccessoryView in my SwiftUI View and add my custom input view as a subview, so that it is always Visible and not an addition to my TextField.
I see two possible solutions to the behavior you want.
In some cases, SwiftUI views move out of the way of the keyboard automatically
in iOS 15 and later you can create an InputAccessoryView in Swiftui
1: In swiftUI, there are several safe areas which views lay themselves inside of by default. One of these is the keyboard safe area. This areas takes up the full screen of the device when the keyboard is hidden but shrinks to the non keyboard area of the screen when the keyboard is displayed. So in the example code below, the text field should move above the keyboard when it appears and drop down when the keyboard disappears (this does not work on an iPad when the keyboard is in the smaller floating mode).
VStack {
ScrollView {
ForEach(0 ..< 50) { item in
Text("Demo Text")
.frame(maxWidth: .infinity)
}
}
TextField("Enter Text", text: $messageText)
}
2: In iOS 15+, you can create a toolbar in the keyboard location. This essentially acts as an InputAccessoryView does in UIKit. The difference between this and method 1 is that a view in here will only appear when the keyboard is displayed. The one expiation to this is when a wired or wireless keyboard is attached to the iPhone or iPad, the toolbar view will still be displayed just at the bottom of the screen.
.toolbar {
ToolbarItemGroup(placement: .keyboard) {
Text("Apears at top of keyboard")
}
}
So putting 1 and 2 together, here is an example that implements both. You can run it in Xcode to help understand how both methods behave
VStack {
ScrollView {
ForEach(0 ..< 50) { item in
Text("Demo Text")
.frame(maxWidth: .infinity)
}
}
TextField("Enter Text", text: $messageText)
}
.toolbar {
ToolbarItemGroup(placement: .keyboard) {
Text("Apears at top of keyboard")
}
}
I know there is a focus handler in TextField - onEditingChanged. But I am using a SecureField along with the TextField (used in a context of password hide and show) i need to get the focus in and focus out events of both SecureField and TextField for performing an animation. I can achieve this using onEditingChanged for TextField but how about SecureField?
Note : I am working on IOS app dev. So 'focusable' cannot be used
Thanks in advance
SecureField do not have onEditingChanged and this is a small little trick that you can do is to continue using Textfield but change the foregroundColor to clear so that it is not visible to the user. In this way, you can do the thing you want.
ZStack {
SecureField("", text: $text)
.font(.system(size: fontSize))
TextField("", text: $text, onEditingChanged: { focused in
.....
})
.foregroundColor(.clear)
.disableAutocorrection(disableAutocorrection)
.font(.system(size: fontSize))
}