Mac Catalyst cannot capture .command key modifier with pressesBegan override - macos

I'm developing a remote desktop control application for iOS (and MacOS through Mac Catalyst) that must be able to capture all keyboard input on the device including the Cmd key (equivalent to Super / Start key on non Mac keyboards) when the app is in the foreground in order to send them to the remote desktop.
I have not yet tried to see if an iOS device with an external keyboard sees the .command key modifier, but when I enabled Mac Catalyst support and installed the app on my Mac and added the following methods to AppDelegate:
override func pressesBegan(_ presses: Set<UIPress>,
with event: UIPressesEvent?) {
super.pressesBegan(presses, with: event)
print(presses.first?.key, presses.first?.key?.modifierFlags)
}
override func pressesEnded(_ presses: Set<UIPress>,
with event: UIPressesEvent?) {
super.pressesEnded(presses, with: event)
print(presses.first?.key, presses.first?.key?.modifierFlags)
}
override func pressesCancelled(_ presses: Set<UIPress>,
with event: UIPressesEvent?) {
super.pressesCancelled(presses, with: event)
print(presses.first?.key, presses.first?.key?.modifierFlags)
}
I was able to capture pretty much any key combination I try except when the Cmd/Start/Super key is also in the key-combination. When the Cmd key is in the key combination or pressed alone, there is absolutely nothing sent to the app. The event appears to be reserved and consumed by Mac OS X completely.
For completeness to this post, I'd like to add that I tried removing all the menus from the app as well just in case the menu was to blame for consuming the Cmd key events, but nothing changed:
override func buildMenu(with builder: UIMenuBuilder) {
if builder.system == .main {
builder.remove(menu: .edit)
builder.remove(menu: .format)
builder.remove(menu: .help)
builder.remove(menu: .file)
builder.remove(menu: .window)
builder.remove(menu: .view)
let dummyCommand = UICommand(title: "Dummy",
action: #selector(dummy),
discoverabilityTitle: "dummy")
let mainMenu = UIMenu(title: "Dummy", image: nil, identifier: UIMenu.Identifier("dummy"), options: .displayInline, children: [dummyCommand])
builder.replace(menu: .application, with: mainMenu)
}
}
I've also tried putting the app into fullscreen mode to no avail.
Any other suggestions on how I can capture the .command modifier?
Next I'm going to try capturing input through the AppKit bundle, but that's not ideal.
Thank you very much!

Hopefully you have an answer already, but in case not:
You will want to inspect the press with code like below
override func pressesBegan(_ presses: Set<UIPress>, with event: UIPressesEvent?) {
var didHandleEvent : Bool = true
print("presses began \(presses.count)")
for press in presses {
guard let key = press.key else { continue }
if key.modifierFlags.contains(.command) {
commandDown = true
print("commandDown")
}
...
You should also only call super.pressesBegan if you want the system to handle the keypress in addition to your code. In your case, I suspect that you don't want the system to do this, so track what presses you have handled, and only if not handled would you call super.
You will also likely want to set your view controller to .becomeFirstResponder()
(assuming you are using a UIVIewController )

Related

How to get state restoration to restore all windows/tabs on macOS?

I have a non-document based macOS AppKit app. It has one window instantiated automatically by the storyboard. I have sub-classed NSWindowController and added a override func newWindowForTab(_ sender: Any?) to enable the + button on the tab-bar. My main view controller lets the user rename the tab title and the window title is set to the same. This is kind of like how Xcode tab renaming works.
Additionally I have sub-classed NSWindow and added a restorableStateKeyPaths to ensure tab and window titles are automatically restored on app restart.
This all works great.
But only for the first tab. The main window is loaded and it has the tab and window titles set automatically.
The other tabs (windows) are not restored.
Any hints on what I miss to make all tabs restored?
My NSWindowController:
class MyWindowController: NSWindowController {
var subview: MyWindowController?
#IBAction override func newWindowForTab(_ sender: Any?) {
let story = self.storyboard
let windowVC = story?.instantiateInitialController() as! Self
window?.addTabbedWindow(windowVC.window!, ordered: .above)
subview = windowVC
windowVC.window?.orderFront(self.window)
windowVC.window?.makeKey()
}
}
My NSWindow:
class MyWindow: NSWindow {
override class var restorableStateKeyPaths: [String] {
return [ "self.tab.title", "self.title" ]
}
}
First you need to make sure that state restoration is enabled for your user, you can do this by going to Preferences->General and unchecking "Close windows when quitting an app".
Then you should use a restoration class in order to restore all open windows.
Basically if an NSWindow doesn't have a restoration class it won't be preserved across launches, that includes your storyboard loaded window. In this case what is happening is Cocoa is ignoring all window preservation because you haven't defined a restoration class for any of your windows so it resorts to its default behavior which is loading the initial storyboard controller.
Implementing restoration class is easy, just create a restoration class that inherits from NSObject and conforms to NSWindowRestoration, then implement its only required type method restoreWindow(identifier:state:completionHandler) like so:
class MyAppWindowRestoration: NSObject, NSWindowRestoration {
static func restoreWindow(withIdentifier identifier: NSUserInterfaceItemIdentifier,
state: NSCoder,
completionHandler: #escaping (NSWindow?, Error?) -> Void) {
// 1.- Retrieve and show the window
// Retrieve a new instance of the only window
let window = (NSStoryboard.main?.instantiateInitialController() as? NSWindowController)?.window
// Call the completion handler with the window and no errors
completionHandler(window, nil)
}
}
Then just assign this class as the window restoration class on every window you want restored, you can do this everywhere after the window has loaded:
window.restorationClass = MyAppWindowRestoration.self
Unfortunately Apple's documentation on state restoration completely sucks so if you have any more questions let me know ;)

Mac NSTextField won't resign firstResponder

I have a window with some NSTextFields. When I click in one and edit the value and press return, I want the focus to go back to what it was before. I don't want the blue ring around the text field and I don't want further keystrokes going to that text field. I would have thought this would happen automatically.
I tried these, and none of them work
sender.resignFirstResponder()
sender.window?.makeFirstResponder(nil)
InspectorWindowController.window?.makeFirstResponder(nil)
AnotherWindowController.window?.becomeFirstResponder()
I'm doing these at the end of my IBAction associated with the text field. Maybe I have to do it from somewhere else?
Thanks
I figured this out. I guess the sent action is happening on another thread. So you have to call makeFirstResponder using Dispatch async.
DispatchQueue.main.async { //omg
sender.window?.makeFirstResponder(nil)
}
I needed to dismiss first responder in my SwiftUI macOS app and here what I found working in a way I need:
func controlTextDidEndEditing(_ obj: Notification) {
DispatchQueue.main.async {
guard let window = self.textField.window else {
return
}
// https://stackoverflow.com/questions/5999148/how-to-determine-whether-an-nssearchfield-nstextfield-has-input-focus
// We need to make sure that our text field is still first responder.
guard let textView = window.firstResponder as? NSTextView,
textView.delegate === self.textField else {
return
}
window.makeFirstResponder(nil)
}
}

Is there any way to disable dictation support of a UITextField?

So I'm working on a tvos app in swift and I was wondering if it's possible to disable dictation support for a custom UITextField. It doesn't really work well for it and I don't want the user to be able to do so
Did you try to use the textfield's keyboardType property? Maybe you can change the text input type, so the dictation function is automatically not shown.
Documentation: https://developer.apple.com/library/tvos/documentation/UIKit/Reference/UITextInputTraits_Protocol/index.html#//apple_ref/occ/intfp/UITextInputTraits/keyboardType
This is a Swift 4 solution based on #BadPirate's hack. It will trigger the initial bell sound stating that dictation started, but the dictation layout will never appear on the keyboard.
This will not hide the dictation button from your keyboard: for that the only option seems to be to use an email layout with UIKeyboardType.emailAddress.
In viewDidLoad of the view controller owning the UITextField for which you want to disable dictation:
// Track if the keyboard mode changed to discard dictation
NotificationCenter.default.addObserver(self,
selector: #selector(keyboardModeChanged),
name: UITextInputMode.currentInputModeDidChangeNotification,
object: nil)
Then the custom callback:
#objc func keyboardModeChanged(notification: Notification) {
// Could use `Selector("identifier")` instead for idSelector but
// it would trigger a warning advising to use #selector instead
let idSelector = #selector(getter: UILayoutGuide.identifier)
// Check if the text input mode is dictation
guard
let textField = yourTextField as? UITextField
let mode = textField.textInputMode,
mode.responds(to: idSelector),
let id = mode.perform(idSelector)?.takeUnretainedValue() as? String,
id.contains("dictation") else {
return
}
// If the keyboard is in dictation mode, hide
// then show the keyboard without animations
// to display the initial generic keyboard
UIView.setAnimationsEnabled(false)
textField.resignFirstResponder()
textField.becomeFirstResponder()
UIView.setAnimationsEnabled(true)
// Do additional update here to inform your
// user that dictation is disabled
}

xcode tvos app exiting issue when overriding menu button

I am currently writing a tvOS app. I've been detecting and overriding the menu button with tapRecognizer to switch between storyboards and other functions. My issue is when I am on my home screen and press menu it does not exit the app. Instead it remembers the last function I used when overriding the menu button and performs that function. Any thoughts on how to clear the tapRecognizer? Or a function that will exit the app?
I'm overriding the menu button with
in Storyboard1
tapRecognizer = [[UITapGestureRecognizer alloc]initWithTarget:self action:#selector(home)];
tapRecognizer.allowedPressTypes = #[[NSNumber numberWithInteger:UIPressTypeMenu]];
[self.view addGestureRecognizer:tapRecognizer];
in my home subroutine I send the user back to my home page storyboard. But from then on the menu button will not exit the app but send me back to storyboard1.
thanks,
SW
Instead of using your own gesture recognizer, override pressesBegan:
override func pressesBegan(presses: Set<UIPress>, withEvent event: UIPressesEvent?) {
if(presses.first?.type == UIPressType.Menu) {
// handle event
} else {
// perform default action (in your case, exit)
super.pressesBegan(presses, withEvent: event)
}
}
If you are using UIGestureRecognizer instead of responding to presses, all you need to do is to disable the recognizer:
tapRecognizer.enabled = NO;
So if no recognizer with UIPressTypeMenu is listening, tvOS suspends the app and displays the home screen. (I've tested this)
You have to override 2 methods to prevent exiting app by pressing Menu button.
Here is ready-to-use template:
override func pressesBegan(presses: Set<UIPress>, withEvent event: UIPressesEvent?) {
for press in presses {
switch press.type {
case .Menu:
break
default:
super.pressesBegan(presses, withEvent: event)
}
}
}
override func pressesEnded(presses: Set<UIPress>, withEvent event: UIPressesEvent?) {
for press in presses {
switch press.type {
case .Menu:
//Do some staff there!
self.menuButtonPressed()
default:
super.pressesEnded(presses, withEvent: event)
}
}
}
If you overwrite the menu button, the app won't be accepted:
EDIT: You can overwrite, but the menu button has to work as a back button to homescreen from the entry point of the app.
10.1 Details
The Menu button on the Siri Remote does not behave as expected in your
app.
Specifically, when the user launches the app and taps the Menu button
on the Siri remote, the app does not exit to the Apple TV Home screen.
Next Steps
Please revise your app to ensure that the Siri remote buttons behave
as expected and comply with the Apple TV Human Interface Guidelines.
It may be help you...
it is swift code.
let menuPressRecognizer = UITapGestureRecognizer()
menuPressRecognizer.addTarget(self, action: #selector(YourViewController.menuButtonAction(_:)))
menuPressRecognizer.allowedPressTypes = [NSNumber(integer: UIPressType.Menu.hashValue)]
self.view.addGestureRecognizer(menuPressRecognizer)
As per Apple's documentation, for custom press handling, we should override all four of these methods-
- (void)pressesBegan:(NSSet<UIPress *> *)presses withEvent:(nullable UIPressesEvent *)event NS_AVAILABLE_IOS(9_0);
- (void)pressesChanged:(NSSet<UIPress *> *)presses withEvent:(nullable UIPressesEvent *)event NS_AVAILABLE_IOS(9_0);
- (void)pressesEnded:(NSSet<UIPress *> *)presses withEvent:(nullable UIPressesEvent *)event NS_AVAILABLE_IOS(9_0);
- (void)pressesCancelled:(NSSet<UIPress *> *)presses withEvent:(nullable UIPressesEvent *)event NS_AVAILABLE_IOS(9_0);
This is the official documentation from XCode:
Generally, all responders which do custom press handling should
override all four of these methods.
Your responder will receive either pressesEnded:withEvent or pressesCancelled:withEvent: for each
press it is handling (those presses it received in pressesBegan:withEvent:).
pressesChanged:withEvent: will be invoked for presses that provide
an analog value
(like thumbsticks or analog push buttons)
*** You must handle cancelled presses to ensure correct behavior in
your application. Failure to
do so is very likely to lead to incorrect behavior or crashes.
SwiftUI Seekers:
I don't know how much this answer helps, but just adding available actions in SwiftUI.
YourAnyView
.onExitCommand(perform: {
print("onExitCommand")
})
.onMoveCommand { direction in
print("onMoveCommand", direction)
}
REF:
onmovecommand
onexitcommand

How to get resolution change event in swift?

I try to make an app, and now i shoud make some changes when screen resolution will change, but i coudn't find how to intercept this event.
Do you have any ideea how can i take that event?
The NSApplicationDidChangeScreenParametersNotification is posted when the configuration of the displays attached to the computer is changed, so
you can register for that notification, e.g. with
NSNotificationCenter.defaultCenter().addObserverForName(NSApplicationDidChangeScreenParametersNotification,
object: NSApplication.sharedApplication(),
queue: NSOperationQueue.mainQueue()) {
notification -> Void in
println("screen parameters changed")
}
Note that there can be various reasons why this notification is
fired, e.g. a change in the dock size (as observed in Cocoa Dock fires NSApplicationDidChangeScreenParametersNotification), so you have to
"remember" the old resolution and compare it with the new resolution.
Swift 4:
The didChangeScreenParametersNotification is posted when the configuration of the displays attached to the computer is changed.
Inside the func applicationDidFinishLaunching() in AppDelegate class or func viewDidLoad() in ViewController class, insert the following code:
NotificationCenter.default.addObserver(forName: NSApplication.didChangeScreenParametersNotification,
object: NSApplication.shared,
queue: OperationQueue.main) {
notification -> Void in
print("screen parameters changed")}
I personally, used it to center the position of my application when switching between the Mac and the external screen.
Here is the updated Swift 3 code:
NotificationCenter.default.addObserver(forName: NSNotification.Name.NSApplicationDidChangeScreenParameters,
object: NSApplication.shared(),
queue: OperationQueue.main) {
notification -> Void in
print("screen parameters changed")
}
Code for Swift 5+
NotificationCenter.default.addObserver(
forName: NSNotification.Name(rawValue: "NSApplicationDidChangeScreenParametersNotification"),
object: NSApplication.shared,
queue: .main) { notification in
self.adjustUIIfNeeded()
}

Resources