Overriding pressBegan and menu button on tvOS - swift

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

Related

How to limit a process between LongPressGesture-began and LongPressGesture-ended

The requirement is simple: I want to start an activity (e.g. drawing circles on the screen) when the user touches the screen and holds down the finger. This activity shall stop immediately when the finger is lifted again. So I used the UILongPressGestureRecognizer. This produces a .began and .ended event. But the problem is that when an activity is launched based on the .began event, the .ended event will not happen as long as the activity is ongoing. My intention for the .ended event is that it shall stop the launched activity. In this way the activity execution would be limited to the time between finger touch down and lift.
I enhanced my sample code to even 2 independent UILongPressGestureRecognizers which can work simultaneously using the UIGestureRecognizerDelegate construct. The simultaneous function of the two recognizers works. However, when one of them launches the activity also the second recognizer does not receive events until the activity has ended.
How can this trivial request be realized?
Here is my sample code. The activity is simplified to 10 print statements. 1 per second. When the touch finger is lifted after 2 seconds, the loop will continue for the remaining 8 seconds.
import UIKit
class ViewController: UIViewController {
var fingerDown = false
#IBOutlet var MyLongPress1: UILongPressGestureRecognizer!
#IBOutlet var MyLongPress2: UILongPressGestureRecognizer!
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func handleLongPress1(_ gesture: UILongPressGestureRecognizer) {
if (gesture as AnyObject).state == .began {
print("longPress1 began")
fingerDown = true
myActivity()
} else if (gesture as AnyObject).state == .ended {
print("longPress1 ended")
fingerDown = false
}
}
#IBAction func handleLongPress2(_ gesture: UILongPressGestureRecognizer) {
if (gesture as AnyObject).state == .began {
print("longPress2 began")
fingerDown = true
} else if (gesture as AnyObject).state == .ended {
print("longPress2 ended")
fingerDown = false
}
}
func myActivity() {
var count = 0
while fingerDown && count < 10 {
sleep(1)
count += 1
print("in myActivity \(count)")
}
}
}
extension ViewController: UIGestureRecognizerDelegate {
func gestureRecognizer(
_ gestureRecognizer: UIGestureRecognizer,
shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer
) -> Bool {
return true
}
}
Any good advice would be appreciated!
You are executing synchronous code in your function.
Instead, what you should do is create an asynchronous, cancellable operation, that starts on .began event, and cancel it on .ended event. Code for this would be too complicated to write and put here in an answer. You can instead refer a concurrently / asynchronous code tutorial like this one to properly implement what you are trying to do.
A much simpler, but dirtier solution that will still work is to simply call your function asynchronously, so it doesn't block the main thread:
DispatchQueue.global().async {
self.myActivity()
}
Having an async call like this in your code is ok, but there are other problems with the code which make it not very clean.
e.g. your code depends on an additional fingerDown state instead of actual state of gesture; use of sleep etc..

Swift 5 - Mac OS - NSTrackingArea overlapping views

Currently I have a little issue when it comes to buttons(NSButton) which have a tracking area and views(NSView overlay) above these buttons, this is my setup:
Custom button:
class AppButton: NSButton {
override func updateTrackingAreas() {
super.updateTrackingAreas()
let area = NSTrackingArea(
rect: self.bounds,
options: [.mouseEnteredAndExited, .activeAlways],
owner: self,
userInfo: nil
)
self.addTrackingArea(area)
}
override func mouseEntered(with event: NSEvent) {
NSCursor.pointingHand.set()
}
override func mouseExited(with event: NSEvent) {
NSCursor.arrow.set()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
An instance of this button class is used i a very basic NSView.
When I hover over the button, the cursor changes correctly.
When I click the button a new overlay(NSView) is opened above the button...
This is where the problem starts:
When I hover over the overlay where my button is placed, the cursor still changes...
I did not know that a NSTrackingArea is going through all views..
How can i solve this issue?
Can I set any property on the overlay(NSView) to somehow disable the NSTrackingArea on the button?
Thanks!!
You can subclass NSView and add local monitoring for events. Check if the event has occurred over the view and if true return nil. This will avoid propagating the events being monitored. If the event is outside the view frame you can propagate it normally returning the event monitored.
class CustomView: NSView {
override func viewWillMove(toSuperview newSuperview: NSView?) {
super.viewWillMove(toSuperview: newSuperview)
wantsLayer = true
layer?.backgroundColor = NSColor.windowBackgroundColor.cgColor
layer?.borderWidth = 1
NSEvent.addLocalMonitorForEvents(matching: [.mouseEntered, .mouseExited, .leftMouseDown]) { event in
if self.frame.contains(event.locationInWindow) {
// if cursor is over the view just return nil to do not propagate the events
return nil
}
return event
}
}
}
If you are trying to stop mouse events from a viewController, add this code in viewDidLoad
NSEvent.addLocalMonitorForEvents(matching: [.mouseEntered, .mouseExited, .leftMouseDown]) { event in
if self.view.frame.contains(event.locationInWindow) {
return nil
}
return event
}

tvOS PressesBegan called too late (after focus already changes)

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.

Prevent NSPopUpButton to open its menu

I need to prevent the popup's menu to be opened when some conditions are met, so I implemented "willOpenMenu" to know when the menu is about to be opened:
class MyPopUpButton: NSPopUpButton {
override func willOpenMenu(_ menu: NSMenu, with event: NSEvent) {
print("willOpenMenu")
// it fires, but what I can do to prevent menu to be opened??
}
}
How can I prevent, now, the menu to not show up?
EDIT
below, in order, what happens when you click on a popup (Type column), when a "Value" column is under editing
Finally I found the way:
override func willOpenMenu(_ menu: NSMenu, with event: NSEvent) {
if (self.outline != nil) {
/*
the following is needed when user click on the type column:
He can have ongoing edits on some rows, so changing first responder
We can aesily inform the outline that We're playing with something else
and have to force ask the user in case edits are wrong.
This also prevent a crash.
*/
if (self.outline?.wrongValue)! {
// this grant the popup menu to not showup (or disappear so quickly)
menu.cancelTrackingWithoutAnimation()
}
self.window?.makeFirstResponder(self)
self.window?.makeFirstResponder(self.outline)
}
}
self.outline is a get/set variable. "wrongvalue" works this way:
override func controlTextDidBeginEditing(_ obj: Notification) {
if obj.object is MyTextField {
self.outline.wrongValue = true
}
}
override func controlTextDidEndEditing(_ obj: Notification) {
if obj.object is MyTextField {
/*
some code here to check if stringValue is ok for the selected tag, otherwise
show the relative alert, the popup menu will not show up anymore
because cancelTracking is called on it
*/
self.outline.wrongValue = false
}
}
cancelTracking() was the key!

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
}