How to use .onSubmit() with vertical TextField in SwiftUI? - ios

I'm trying to have a vertically growing TextField in `SwiftUI but also have the software keyboard have a custom submission method.
This uses the new functionality of iOS 16's TextFields being able to take an axis as an argument for which way it should grow. See the docs here: https://developer.apple.com/documentation/swiftui/textfield/init(_:text:axis:)-8rujz.
Here's a sample ContentView showing the setup.
struct ContentView: View {
#State var message: String = ""
var body: some View {
VStack {
Text("Try to submit this using the blue send button on the software keyboard")
TextField("Placeholder", text: $message, axis: .vertical)
.onSubmit {
print("submission!")
}
.submitLabel(.send)
}
}
}
When you run this, you can see the TextField properly grows vertically, but even when you have a custom submission label, pressing the blue "send" button in the software keyboard on iOS just inserts a newline, rather than firing the .onSubmit
When using a hardware keyboard, pressing the return does run the code in .onSubmit, so this seemingly just a limitation of the software keyboard.

Related

Present sheet with a TextField and its keyboard in a single animation?

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.

How to add more padding below a TextView when the keyboard is shown and enable button clicks in SwiftUI

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?

SwiftUI Picker iOS 16 not filling available space

I am using the following code (example) to render a SwiftUI Picker on iOS:
let strings: [String] = ["short", "very, ver long string"]
#State var selectedString: String = ""
Form {
Picker("Method", selection: $selectedString) {
ForEach(strings, id: \.self) { string in
Text(string)
}
}
}
In iOS 16 the design of the menu style picker has changed (it now includes 2 small chevrons), which is all good, except it no longer fills the available width (as it did on iOS 15). This results in longer strings flowing onto multiple lines even when this isn't neccessary.
Short String (all fine):
Long String (not so good):
I have tried .fixedSize(), which works to some extend but if the string does in fact need to be on two lines this forces the label to be squished. If I add a background to the Picker, it is clear that it only fills around 1/3 of the available space.
Does anyone have any suggestions?
Separate the label from the Picker and wrap it in a HStack.
Form {
HStack {
// the new label text
Text("Method")
.fixedSize() // give other views in HStack space to grow
// push the external label and Picker to the leading and trailing view edges
Spacer()
Picker("Method", selection: $selectedString) {
ForEach(strings, id: \.self) { string in
Text(string)
}
}
.labelsHidden() // the label is in the Text view
}
}
Hide the Picker label by using the .labelsHidden() modifier.
Use the .fixedSize() modifier on the new Text. This will allow the Picker to expand to fit all its contents.
Use Spacer between Text label and Picker to push both items to the edge.

SwiftUI InputAccessoryView

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")
}
}

Text selection breaks scrolling

I'm trying out iOS 15 with Xcode Beta 2, and I'm using the new textSelection feature for Text.
However, if you enable text selection and put the Text objects in a List, and try scrolling on the text, nothing happens. However, disabling the text selection allows scrolling anywhere on the list.
Here is some reproducible code:
struct ContentView: View {
#State var data = (0..<100).map { "Test: \($0)" }
var body: some View {
List(data, id: \.self) { str in
Text(str)
.textSelection(.enabled)
}
}
}
If you try scrolling by moving your finger up on the area with text, nothing happens. However you can still scroll on the white areas.
Disabling text selection fixes this issue.
Are there any possible workarounds?

Resources