I’m having trouble using an arrow key as a .keyboardShortcut in SwiftUI. Sample iOS app:
struct ContentView: View {
#State var time: Date = Date()
var body: some View {
VStack {
Button("Press Me") {
time = Date()
}
.keyboardShortcut(KeyEquivalent.rightArrow, modifiers: [])
Text("\(time)")
}
}
}
This puts up a button that, when pressed, changes the time displayed in the text. I should be able to use the right arrow key on the keyboard and get it to work as well, but it doesn’t. If I change the keyboardShortcut line to, say, this:
.keyboardShortcut(KeyEquivalent(“a”), modifiers: [])
everything works as expected. You can press the “a” key and the time changes. If you hold down the command key, you get the system-provided HUD that shows the “a" shortcut. Change it to .rightAarrow and it shows the HUD but there’s an enclosed “?” for the shortcut, and the shortcut doesn’t fire when the arrow key is pressed.
(I’m aware I could do this using UIKit. Trying to understand why the SwiftUI version doesn’t work.)
I am attempting to accomplish the same objective in my MacOS SwiftUI app. Using your code as an example, I inserted the .keyboardShortcut(KeyEquivalent.rightArrow, modifiers: []) after my Button{} and it works fine. I then pasted your entire code into my ContentView and again it works fine. I do not know why it works in my MacOS app but not in your iOS app.
Copying my answer from this post. I wasn't able to use SwiftUI's commands to get this to work on iOS/iPadOS. However I found some luck using view controllers, and if you're using SwiftUI views then this will work with a hosting controller.
In your view controller, add the code below. The important bit is setting wantsPriorityOverSystemBehavior to true:
override var keyCommands: [UIKeyCommand]? {
let upArrow = UIKeyCommand(input: UIKeyCommand.inputUpArrow, modifierFlags: [], action: #selector(test))
upArrow.wantsPriorityOverSystemBehavior = true
return [upArrow]
}
#objc func test(_ sender: UIKeyCommand) {
print(">>> test was pressed")
}
Related
Why is my SwiftUI Swipe Action behaving like this?
I don't now how to add a GIF in stack overflow so here is a imagur link https://imgur.com/a/9MqjIgX.
If you don't want to click on external links here is a image from the GIF:
My View:
struct MyView: View {
#State var shops = [Shop.empty(), Shop.empty(), Shop.empty(), Shop.empty(), Shop.empty()]
var body: some View {
NavigationView {
List($shops) { $shop in
Text(shop.name)
.swipeActions {
Button {
shop.toggleFavourite()
} label: {
Image(systemName: "star")
}
}
}
}
}
}
the shop struct:
struct Shop: Hashable, Identifiable {
var id: UUID
var favourite: Bool
init(id: UUID){
self.id = id
self.favourite = UserDefaults.standard.bool(forKey: id.uuidString)
}
mutating func toggleFavourite() {
favourite.toggle()
UserDefaults.standard.set(favourite, forKey: id.uuidString)
}
static func empty() -> Shop{
Shop(id: UUID())
}
}
But I can't sadly I can't give you a working example, because I tried to run this code in a fresh app and it worked, without the Bug. On the same device. And I don't understand why, because I also put this view in the root of my old project, just for testing, and the bug stayed there.
I was able to figure out, that if I commented out this line:
UserDefaults.standard.set(favourite, forKey: id.uuidString)
my code would work. But unfortunately I can't just leave out this line of code.
I tried several things, including wrapping this line into DispatchQueue.main.async {} and DispatchQueue.main.sync {}, same with the DispatchQueue.global(). I also added delays. Short delays wouldn't work at all (under .5 seconds) and longer delays would just delay the view bug.
Of course I also tried wrapping this line into a separate function, and so on.
There are two mayor points, why I'am so confused:
Why is the line, that sets this to the Userdefaults even influencing the view? I mean I checked with a print statement, that the initializer, which is the only section in my code that checks this Userdefaultvalue, only gets called when the view gets initialized.
Why does the code work in a different project?
I know since I can't provide a working example of my bug it's hard for you to figure out whats wrong. If you have any ideas, I would be very happy!
I have a very basic onboarding for my app where there are few screens in a tabview with the pagetabviewstyle, and I am having trouble getting the keyboard avoidance behavior to work.
If I have have the tabview set with .ignoresSafeArea, then the keyboard doesn't avoid my textfields.
[Tab View With IgnoresSafeArea][1]
[Page With TextField][2]
[Before TextField Pressed][3]
[Keyboard Covers TextField][4]
If I remove the .ignoresSafeArea then the keyboard pushes the pagetabview dot indicators up the screen into the middle of the textfield.
[Keyboard Avoids With Page Dots Pushed Into TextField][5]
My ideal outcome would be to have the entire TabView IgnoreSafeArea without disabling the keyboard avoidance. I've checked online for a while without seeing any information on this issue. Thanks in advance for the help and suggestions!
[1]: https://i.stack.imgur.com/flKCY.png
[2]: https://i.stack.imgur.com/lNSBR.png
[3]: https://i.stack.imgur.com/DWZzN.jpg
[4]: https://i.stack.imgur.com/9ktwz.jpg
[5]: https://i.stack.imgur.com/Wwbwz.png
I had the same issue and solved it by changing the tabViewStyle whenever the keyboard state changes. Publisher code from How to detect if keyboard is present in swiftui :
var keyboardPublisher: AnyPublisher<Bool, Never> =
Publishers.Merge(
NotificationCenter.default
.publisher(for: UIResponder.keyboardWillShowNotification)
.map { _ in true },
NotificationCenter.default
.publisher(for: UIResponder.keyboardWillHideNotification)
.map { _ in false }
)
.eraseToAnyPublisher()
Create a state var in your view:
#State var keyboardShown: Bool
Hook up the publisher:
TabView {
}
.onReceive(vm.keyboardPublisher) { // <-- wherever you put your publisher
self.keyboardShown = $0
}
And modify the tab view style accordingly:
TabView {
}
.tabViewStyle(.page(indexDisplayMode: keyboardShown ? .never : .automatic))
It works surprisingly well.
So for my own reasons, I need the full control that UITextField and its delegates would normally offer, but the screen it's being added to is written with SwiftUI. Needs a .decimal keyboard, so no Return Key.
I have only 2 issues remaining, 1 of which I hope to resolve here. Attempting to add a call to resign first responder and add it to a VStack on the page basically disables the UITextField, since I can't bring up the keyboard.
How can I dismiss this keyboard without adding an arbitrary extra button to the page?
Example Code:
Struct CustomTextView: UIViewRepresentable {
/// Insert init, updateView, binding variable, coordinator, etc
func makeView() -> UITextField {
var textField = UITextField()
textField.delegate = context.coordinator
/// Set up rest of textfield parameters such as Font, etc.
return textField
}
}
extension CustomTextView {
class Coordinator: NSObject, UITextFieldDelegate {
/// UITextfield delegate implementations, extra reference to binding variable, etc
/// Primarily textField.shouldChangeCharactersInRange
}
}
struct ContentView: View {
#State viewModel: ViewModel
var body: some View {
VStack {
CustomTextView($viewModel.parameter)
/// Other views
}
.onTap {
/// Attempting to add the generic call to UIApplication for resignFirstResponder here does not allow CustomTextView to ever hold it even when tapped in
}
}
}
I can't give all the code for privacy reasons, but this should be enough to establish my issue.
I have done this by adding this Function to you view below.
func hideKeyboard() {
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
Then with a ontapGesture you can make the keyboard go away.
For example you can use this on the background Stack of your whole view. If a user taps on the background the keyboard will dissapear.
.onTapGesture {
self.hideKeyboard()
}
So I found a trick on my own with an epiphany overnight.
First, I would like to share to anyone else a very basic reason why inb4cookies solution wasn't quite adequate. While I had already tried adding a resignFirstResponder call like it to the onTap of the background stack, it was triggering the onTap for the VStack when I was clicking the field.
This is likely because I am using a UITextField as the back end for this component and not a SwiftUI TextField.
However, it was partially used in the final solution. I still applied it, but there is an extra step.
VStack {
CustomTextView($viewModel.parameter)
.onTap {/*Insert literally any compiling code here*/ }
/// Other views
}
.onTap {
self.hideKeyboard()
}
You'll see that above, there is an extra onTap. I tested it with a print statement, but this will override the onTap for the VStack and prevent the keyboard from being dismissed right after it is brought up. Tapping anywhere else on the VStack still closes it, except for Buttons. But I can always add hideKeyboard to those buttons if needed.
I'm writing a MacOS document app using the SwiftUI App lifecycle, and all the tricks I see here and elsewhere for sending a menu action to the active window depend on using platform specific implementation, which is (mostly) unavailable in a SwiftUI Lifecycle app. What I'm looking for is something like SideBarCommands(), which adds a menu item that, when selected by mouse or command key, toggles the appearance of the sidebar in the active window. All the Command examples I have seen thus far are trivial, none address a multi-document, multi-window use case.
Given a ContentView declared thusly:
struct ContentView: View {
#Binding var document: TickleDocument
var body: some View {
TextEditor(text: $document.text)
}
public func menuTickle() {
document.text = "Wahoo!"
}
}
and a command, which is added via:
struct TickleApp: App {
public static var target:TickleDocument?
var body: some Scene {
let docGroup = DocumentGroup(newDocument: TickleDocument()) { file in
ContentView(document: file.$document)
}
docGroup
.commands {
CommandMenu("App Tickles") {
Button("Tickle The ContentView") {
// Here's where I need to call menuTickle() on the active ContentView
}.keyboardShortcut("t")
}
}
}
}
}
What do I need to do so the button closure can call menuTickle() on the active ContentView? I know it can be done, because SideBarCommands() does it (unless Apple is using some non-public API to do it...).
For bonus points, tell me how I can detect whether or not I'm the active ContentView while body is being evaluated, and how I can detect when it changes! Tracking the Environment variable scenePhase is worthless - it always reports active, and never changes.
My question is a duplicate of this one.
The answer to that question contains a link to a solution that I have verified works, and can be found here
In a SwiftUI app, I need to set the focus on a TextField and bring the keyboard automatically, in standard Swift this would be done with:
field.becomeFirstResponder()
But this does not seem to exist in SwiftUI.
I found a work around here.
But, my field uses :onCommit; which is not in the sample code.
What is the way to set the :onCommit functionality when using UIViewRepresentable ?
iOS 15+ has a solution for this.
#FocusState combined with the focused(_:) modifier can be used to control first responder status for textfields.
struct ExampleView: View {
#FocusState private var isFocused: Bool
#State private var textInput = ""
var body: some View {
TextField("Example", text: $textInput)
.focused($isFocused)
Button("Confirm") {
if textInput {
isFocused = true
}
}
}
}
For iOS15
There is a solution implemented by apple (as mentioned by #AlphaWulf)
For iOS14
In my opinion, the best approach is to implement your own version of TextField using the UIRepresentable protocol. This might sound like something difficult but it is actually quite simple.
Why it is better to implement your own text field over the solutions using view hierarchy introspection?
One is that a solution based on traversing underlying views is hacky by nature and even a minor iOS version update might break it.
Secondly, in a real-world app, you will want to set additional things on the text field (like return button type and supplementary view) but Apple didn't make a way of doing so and you will be forced to wrap a UITextField in any case.
https://blog.agitek.io/swiftui-2-first-responder-b6a828243268
In this post I have a detailed solution that is similar to what Apple has implemented in SwiftUI 3.
There is an open-source project for your needs, at https://github.com/mobilinked/MbSwiftUIFirstResponder
TextField("Name", text: $name)
.firstResponder(id: FirstResponders.name, firstResponder: $firstResponder, resignableUserOperations: .all)
TextEditor(text: $notes)
.firstResponder(id: FirstResponders.notes, firstResponder: $firstResponder, resignableUserOperations: .all)