tvOS PressesBegan called too late (after focus already changes) - swift

My goal is to call doSomething whenever the user presses "up" on their Apple TV remote, only while the topButton is already focused.
The goal is that doSomething should NOT get called if bottomButton was focused. However, when user presses up, the focus moves up to topButton before calling pressesBegan. This causes doSomething to get called regardless of which button was focused.
func doSomething() {
// goal is this function should only run when topButton is focused
}
override func pressesBegan(_ presses: Set<UIPress>, with event: UIPressesEvent?) {
guard presses.first?.type == UIPress.PressType.upArrow else { return }
print(self.topButton.isFocused)
print(self.bottomButton.isFocused)
if self.topButton.isFocused {
doSomething()
}
}
Note that this workaround doesn't work:
override func didUpdateFocus(in context: UIFocusUpdateContext, with coordinator: UIFocusAnimationCoordinator) {
guard let previouslyFocusedItem = context.previouslyFocusedItem as? UIButton else { return }
if previouslyFocusedItem == topButton {
self.doSomething()
}
}
Because the focus doesn't update if user presses up while on topButton.
Which leads to more complicated code (would have to add properties and weird-looking if-statements between the two functions that confuse other developers). Is there a more efficient workaround?

Was able to solve it using UIFocusGuide instead. Another issue I found with pressesBegan was that it didn't detect if user swipes up. UIFocusGuide handles both cases.

Related

UIPress.key is nil

You may experience a problem where UIPress.key is nil when you are building a Catalyst app for macOS 11 Big Sur and trying to evaluate keyboard input. The code to reproduce the issue is following (in any UIViewController):
#if targetEnvironment(macCatalyst)
override func pressesEnded(_ presses: Set<UIPress>, with event: UIPressesEvent?) {
//
for press in presses {
guard let key = press.key else { continue }
NSLog("Key Pressed: %#", key.charactersIgnoringModifiers) // <-- will never be logged on Big Sur
}
}
#endif
Running this with Catalina will work, but fails on Big Sur. I have opened a bug report to Apple since I thought this is a bug with Big Sur, but the answer to solve the problem was quite easy:
Simply implement the override for pressesBegan, and it starts to work. So, adding this:
override func pressesBegan(_ presses: Set<UIPress>, with event: UIPressesEvent?) {
//
for press in presses {
guard let key = press.key else { continue }
NSLog("Key press began: %#", key.charactersIgnoringModifiers)
}
}
and the issue is solved.
That's it! I want to share it here, because you may suffer from same issue. But I have also a question to you: For me it's somewhat weird that I need to implement another override to make the desired override working. Do you know the reason here? Is this anywhere documented?
Thanks much.
Also important to note that if you callsuper.pressesEnded(presses, with: event) before accessing the value of UIPress.key then UIPress.key will be nil.

OSX swift 3 - can't disable keyDown. error sound

Im am trying to disable error sound when I push down space bar and arrow keys. I tryed handling events with super.keyDown(with: event) no luck. Cant find any other working solutions apart from using global key frameworks. Are there any other options I have?
NSEvent.addLocalMonitorForEvents(matching: .keyDown) { (aEvent) -> NSEvent? in
self.keyDown(with: aEvent)
return aEvent
}
}
override func keyDown(with event: NSEvent) {
super.keyDown(with: event)
}
Update: I've found out that the root cause of the problem was, that a view was the first responder that shouldn't be actually. After setting the responder to nil self.view.window?.makeFirstResponder(nil) I was able to fix this. I've also used performKeyEquivalent as this answer suggested.
I know my answer is very late, but maybe it will help you or someone else in the future. I'm not sure if that's the best way to do it but it does work. Simply return nil instead of the event.
NSEvent.addLocalMonitorForEvents(matching: .keyDown) { (aEvent) -> NSEvent? in
self.keyDown(with: aEvent)
return nil
}
override func keyDown(with event: NSEvent) {
super.keyDown(with: event)
}
Apple's documentation of the method says the following for the block parameter:
The event handler block object. It is passed the event to monitor. You can return the event unmodified, create and return a new NSEvent object, or return nil to stop the dispatching of the event.
The only downside is, that there will be no sound at all. Even if the key event is not handled by you.

Best strategy in swift to detect keyboad input in NSViewController

I want to detect keyboard input in my NSViewController.
The idea is to have several actions performed if the user presses certain keys followed by ENTER/RETURN.
I have checked if keyDown would be a appropriate way. But I would receive an event any time the user has pressed a key.
I also have though on using an NSTextField, set it to hidden and let it have the focus.
But maybe there are other better solution.
Any ideas?
Thanks
I've finally got a solution that I like.
First it has nothing todo with any hidden UI Elements but rather let the viewcontroller detect keyboard input.
var monitor: Any?
var text = ""
override func viewDidLoad() {
super.viewDidLoad()
self.monitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown, handler: myKeyDownEvent)
}
override func viewWillDisappear() {
//Clean up in case your ViewController can be closed an reopened
if let monitor = self.monitor {
NSEvent.removeMonitor(monitor)
}
}
// Detect each keyboard event
func myKeyDownEvent(event: NSEvent) -> NSEvent {
// keyCode 36 is for detect RETURN/ENTER
if event.keyCode == 36 {
print(text)
text = ""
} else {
text.append( event.characters! )
}
return event
}

How to detect button highlight on Apple TV in Swift

I am making a simple app that will show the user some information when they hover over a button. I have looked all over for an answer for this but it seems that no one has wondered this yet. I know it is possible to detect button highlights because I have seen it in some apps I downloaded on my Apple TV. Here is basically what I'm aiming for:
#IBAction func firstButton(_ sender: Any) {
//This function would be called when the first button is highlighted/hovered over
label.text = "Hi there"
}
#IBAction func secondButton(_ sender: Any) {
//This function would be called when the second button is highlighted/hovered over
label.text = "How are you?"
}
I know that just creating an IBAction func won't do the trick, but I am just using this as an example of what I want to do.
So is there a way to detect button highlights/button hovering and how?
Thanks in advance. Any help is appreciated.
By hover, I think you mean "is the button in focus". There are a couple ways to tell if a UI element is in focus.
For UIViewController you can override didUpdateFocus which provides context about what happening in the focus engine.
override func didUpdateFocus(in context: UIFocusUpdateContext, with coordinator: UIFocusAnimationCoordinator) {
if context.nextFocusedView == myButton {
print("My button is about to be focused")
}
else {
print("My button is NOT in focus")
}
}
Or in a custom UIView (ie. a customUIButton) you can check the isFocused property.
override func didUpdateFocus(in context: UIFocusUpdateContext, with coordinator: UIFocusAnimationCoordinator) {
print(isFocused)
}

Overriding pressBegan and menu button on tvOS

I have one view controller with many subviews. When one presses Menu button, my app should perform some actions. But if there wasn't any "active" actions, my app should close.
override func pressesBegan(presses: Set<UIPress>, withEvent event: UIPressesEvent?) {
guard let type = presses.first?.type else { return }
//...
if falls == true && type == .Menu {
super.pressesBegan(presses, withEvent: event)
}
}
At first time, this code works as should. But if I reopen app and press Menu button, app closes, however super function wasn't called.
How should I get rid of that?
I did a dirty hack:
Removed super call:
super.pressesBegan(presses, withEvent: event)
Call suspend on UIApplication when I need to close the app
if type == .Menu && falls == true {
UIApplication.sharedApplication().performSelector(Selector("suspend"))
}