How to send app to background - objective-c

On tvOS, I've only been able to get the begin states of button presses from the Siri remote by overriding the pressesBegan method on the view. If I use gesture recognizers, it only returns the end state. The catch is, when I override pressesBegan, even if I only use it for the select button, it still overrides the default function of the menu button (to push the app to the background). So I was looking into how to send the app to the background and call that method for the menu button (as is default behavior), but it appears that it is not kosher per Apple's standards to do that.
Here is my code for reference:
-(void)pressesBegan:(NSSet<UIPress *> *)presses withEvent:(UIPressesEvent *)event {
for (UIPress* press in presses) {
switch (press.type) {
case UIPressTypeSelect:
NSLog(#"press began");
break;
case UIPressTypeMenu:
// this is where I would call the send to background call if Apple would allow that
// removing this case also has no effect on what happens
break;
default:
break;
}
}
As an alternative, this ONLY sends button release signals, but nothing presses begin.
UITapGestureRecognizer* tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(gestureTap:)];
tapGesture.allowedPressTypes = #[[NSNumber numberWithInteger:UIPressTypeSelect]];
[view addGestureRecognizer:tapGesture];

When there's some behavior that happens if you don't override a method, and the behavior goes away in an empty override implementation, it stands to reason that behavior is provided by the superclass. (Cocoa is dynamic and complicated, so such inferences aren't true 100% of the time, but often enough.)
So, just call super for the cases where you don't want your override to change the default behavior:
case UIPressTypeMenu:
[super pressesBegan: presses withEvent: event];
break;

Related

How to listen to Apple TV Remote in Objective-C?

This seems like it should be pretty straight-forward but I'm having trouble finding a working example, good documentation, or even many StackOverflow posts that are helpful.
I have a custom view which contains an AVPlayer, like so:
#implementation
{
#private
AVPlayerViewController *controller
}
- (id) init
{
self = [super init];
if (self)
{
controller = [[AVPlayerViewController alloc] init];
controller.view.frame = self.view.bounds;
[self.view addSubview:controller.view];
}
return self;
}
#end
(I have a few other views like a message that overlays the player, a poster that I display while swapping videos, etc - but this is the basic setup)
When I integrated the IMA SDK, I started to have issues. If you press the pause button on the remote control during an ad, it pauses the ad just fine. But if you press the pause button again it doesn't unpause the ad, but instead unpauses my content player behind the ad. I don't hear any audio, but I know the content player was unpaused because I have ID3 metadata in my video and an NSLog() statement when I hit it, and I begin to see these logs. If I press the pause button again, the logs pause. I press it a fourth time, the logs start up again.
To try and fix this I wanted to bind a listener to the remote's play/pause button and make sure that if I was playing an ad then the ad was resumed, not the content. So I tried adding the following to my init method on my view:
UITapGestureRecognizer *tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self.view action:#selector(tapped:)];
[tapRecognizer setAllowedPressTypes:#[ [NSNumber numberWithInt:UIPressTypePlayPause] ]];
[self.view addGestureRecognizer:tapRecognizer];
I then created the following method:
- (void) tapped: (UITapGestureRecognizer *) sender
{
NSLog(#"Tapped");
}
This isn't being called. I'm pretty confident I made a simple mistake, but the documentation isn't very clear, so I'm not sure what I should be doing instead. The official documentation on detecting button presses uses Swift and says:
let tapRecognizer = UITapGestureRecognizer(target: self, action: "tapped:")
tapRecognizer.allowedPressTypes = [NSNumber(integer: UIPressType.PlayPause.rawValue)];
self.view.addGestureRecognizer(tapRecognizer)
I believe I translated those three lines well. The documentation then doesn't show what the tapped method should look like, but instead goes on a tangent about working with low-level event handling. So to get the appropriate method signature I looked at the documentation on UITagGestureRecognizer which had the following (Swift) example for writing a handler:
func handleTap(sender: UITapGestureRecognizer) {
if sender.state == .ended {
// handling code
}
}
This is why I went with - (void) tapped: (UITapGestureRecognizer *) sender
Still, after all of that, it's not working.
Quick Update
I tried replacing:
initWithTarget:self.view
With:
initWithTarget:controller.view
And:
self.view addGestureRecognizer
With:
controller.view addGestureRecognizer
And this time it looks like something actually happened when I pressed the play/pause button. The app crashed and Xcode gave me the following error:
2019-12-17 12:16:50.937007-0500 Example tvOS App[381:48776] -[_AVPlayerViewControllerContainerView tapped:]: unrecognized selector sent to instance 0x10194e060
So it seems like (correct me if I'm wrong):
The AVPlayerViewController has the focus, not my view
The gesture recognizer calls the selector on whatever class you register it to, rather than the class that did the registering
So I guess an alternative question to my original would be: How do I allow my class to handle a gesture on some other class (e.g. AVPlayerViewController)?
The target does not need to equal the view that the gesture recognizer is attached to. You can set the target to MyView but still attach the gesture recognizer to controller.view
To work around the unrecognized selector crash, you need to make sure you're providing the correct object as the target for your gesture recognizer. The way UIGestureRecognizer works is that when the gesture is recognized, it will invoke the given selector on the given target object. In other words, when gesture fires, it's going to perform the equivalent of:
[target selector:self];
(You seem to be treating the target as the view that the gesture will be attached to, which isn't how it works)
So if you implemented tapped: on your class MyView, then the target you pass to the gesture recognizer initializer should be an instance of MyView. You probably want to provide self, not self.view.

Allowing escape to exit full screen as it normally does in Cocoa

So I'm enabling full screen mode in my Cocoa/Mac app, and the default behavior obviously is that you can hit the escape key to exit out of full screen mode. I've added some NSTextViews (inside NSScrollViews), and now they are intercepting the escape key and I'm unable to exit out of full screen.
What's the best way for me to still allow the escape key to exit out of full screen and not be intercepted by my NSTextViews?
Thanks!
The default key binding for Escape is to -cancelOperation:. In response to that, NSTextView then turns around and sends itself -complete: to do text completion.
Is that what you're seeing when you press Escape? Does the text view offer to complete any text that you've typed?
The first thing to try is to disable completion by implementing -textView:completions:forPartialWordRange:indexOfSelectedItem: in the text view's delegate and returning nil.
If that doesn't work, implement -textView:doCommandBySelector: and compare the selector to #selector(cancelOperation:). If it's not equal, return NO to allow normal processing. If it is, try passing it up the responder chain using [[theTextView nextResponder] doCommandBySelector:theSelector]. Then return YES from your delegate method to prevent the text view from trying to handle it.
If that still doesn't work, instead of passing it up the responder chain, check if the app is in full-screen mode by testing if [NSApp presentationOptions] contains NSApplicationPresentationFullScreen. If it is, get it out of full-screen mode by setting a normal set of presentation options. Alternatively, you can test the main window to see if its style mask includes NSFullScreenWindowMask and, if it does, call -toggleFullScreen: on it. Again, you'd return YES from your delegate method to prevent further processing.
Based on Ken's suggestions, here's what I ended up doing to get it to work:
- (BOOL)textView:(NSTextView *)textView doCommandBySelector:(SEL)commandSelector {
if (commandSelector == #selector(cancelOperation:)) {
if([self inFullScreenMode]) {
[self.window toggleFullScreen:nil];
}
}
else {
return NO;
}
return YES;
}
- (BOOL) inFullScreenMode {
NSApplicationPresentationOptions opts = [[NSApplication sharedApplication ] presentationOptions];
if ( opts & NSApplicationPresentationFullScreen) {
return YES;
}
return NO;
}

Override behaviour of pressing "space" in PDF Kit view for OSX

It seems like the standard behaviour of a pdfkit view when pressing space is to switch pages. We want to use the space button for other events (playing audio in our case).
Setting the space as shortcut in the menu does only work before the pdfkit view is interacted with. After that the behaviour is always set to switch pages.
We have also tried to intercept the space key-down-event using this code:
if (!keyDownEventMonitor) {
__weak IBBookViewController *weakSelf = self;
NSEvent * (^monitorHandler)(NSEvent *);
monitorHandler = ^NSEvent * (NSEvent * theEvent){
BOOL handleEvent = weakSelf.view.window != nil
&& theEvent.type == NSKeyDown
&& !theEvent.isARepeat;
if (handleEvent) {
switch ([theEvent keyCode]) {
case 49:
[self audioPlayPauseBtnHit:nil];
break;
default:
break;
}
}
return theEvent;
};
keyDownEventMonitor = [NSEvent addLocalMonitorForEventsMatchingMask:NSKeyDownMask handler:monitorHandler];
}
but it does only extend the space functionlity so both the audio is played and the page is switched.
Any ideas?
You have other directions to go here.
Subclassing to override behavior.
( keeping in mind that the behavior may be inherited from a superclass or part of a class that is a private ivar in the header for the pdfview class. )
Looking at the responder chain and which object really gets the keyDown message first. Intercept and forward, possibly changing key view temporarily.
Creating a cover view that handles the keyDown events you care about first and forwards other events to the responder chain.

Disabling dictation within the app [duplicate]

Ours is a health care app. We have a HIPAA-compliant speech recognizer in the app through which all the dictation can take place. The hospitals don't want physicians to accidentally start speaking to the Nuance Dragon server which is not HIPAA-compliant. So, I was looking for ways I could supress the dictation key on the keyboard.
I tried putting a fake button on the Dictation button on the key pad, but on the iPad the split dock concept keeps moving the microphone all over the screen. This does not sound like a reasonable solution. Are there any experts out there who could help me?
OKAY, finally got it! The trick is to observe UITextInputMode change notifications, and then to gather the identifier of the changed mode (Code seems to avoid the direct use of Private API, though seems to require a little knowledge of private API in general), and when the mode changes to dictation, resignFirstResponder (which will cancel the voice dictation). YAY! Here is some code:
Somewhere in your app delegate (at least that's where I put it)
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(inputModeDidChange:) name:#"UITextInputCurrentInputModeDidChangeNotification"
object:nil];
And then you can
UIView *resignFirstResponder(UIView *theView)
{
if([theView isFirstResponder])
{
[theView resignFirstResponder];
return theView;
}
for(UIView *subview in theView.subviews)
{
UIView *result = resignFirstResponder(subview);
if(result) return result;
}
return nil;
}
- (void)inputModeDidChange:(NSNotification *)notification
{
// Allows us to block dictation
UITextInputMode *inputMode = [UITextInputMode currentInputMode];
NSString *modeIdentifier = [inputMode respondsToSelector:#selector(identifier)] ? (NSString *)[inputMode performSelector:#selector(identifier)] : nil;
if([modeIdentifier isEqualToString:#"dictation"])
{
[UIView setAnimationsEnabled:NO];
UIView *resigned = resignFirstResponder(window);
[resigned becomeFirstResponder];
[UIView setAnimationsEnabled:YES];
UIAlertView *denyAlert = [[[UIAlertView alloc] initWithTitle:#"Denied" message:nil delegate:nil cancelButtonTitle:#"Okay" otherButtonTitles:nil] autorelease];
[denyAlert show];
}
}
you can create your own keyboard and set the inputView for the text fields that will accept this dictation. then when they press any keys they will get your keyboard, therefore you dont have to override the keys on the standard keyboard, you will be able to customize the entire thing.
self.myButton.inputView = self.customKeyboardView;
here is an example of an extremely custom keyboard
http://blog.carbonfive.com/2012/03/12/customizing-the-ios-keyboard/
Ray also has a teriffic tutorial on custom keyboards.
http://www.raywenderlich.com/1063/ipad-for-iphone-developers-101-custom-input-view-tutorial
I hope that helps.
I had the same issue and the only way i found that hides the dictation button is changing the keyboard type.
For me changing it to email type seemed to be reasonable:
textField.keyboardType = UIKeyboardTypeEmailAddress;
You could make a subclass of UITextField/UITextView that overrides insertDictationResult: to not insert anything.
This won't prevent the information being sent, but you could then display an alert informing them of the breech.
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
}

Block until NSAlert (shown as a modal sheet) is dismissed

I'm currently learning (by doing) objective-c by implementing a feature I felt is missing in the Titanium Appcelerator Desktop SDK: A way to do modal dialog with custom button texts and optionally to display them as a "sheet".
All is dandy and working, however, when displaying the NSAlert as a "sheet" my method that's creating the alert returns immediately and that's what I want to prevent.
The method's creating the alert returns an int (the return code from the NSAlert).
The code inside basically boils down to:
int returnCode = -1;
if (displayAsSheet) {
[alert beginSheetModalForWindow:nativeWindow modalDelegate:delegate didEndSelector:#selector(alertDidEnd:returnCode:contextInfo:) contextInfo:nil];
} else {
returnCode = [alert runModal];
}
return returnCode;
The modalDelegate is an object that's implementing the needed:
- (void)alertDidEnd:(NSAlert *)alert returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo;
and for now it just does a NSLog of the returnCode.
So my question is:
How can I block my method from returning until the "sheet" has been dismissed?
Or am I going about this the wrong way?
You must start a modal session for you sheet after showing it and stop the session after closing sheet.
Check this: https://github.com/incbee/NSAlert-SynchronousSheet, I think it will be helpfull.
You could use this after beginSheetModalForWindow:...:
[[NSRunLoop mainRunLoop] runMode:NSModalPanelRunLoopMode beforeDate:[NSDate distantFuture]]
However, it will make any other windows in your app unusable until the sheet is dismissed. It would be better not to block those windows.
my method that's creating the alert returns immediately
I believe that's because, as #Josh says, the sheet is running modal only relative to the window to which it is attached; it is not freezing the entire app. Therefore, as soon as beginSheetModal... executes, the rest of your method continues to run, concluding with return returnCode (here returning -1), without waiting for the user to respond to the alert.
The return code is a stand-in for which button on the alert panel the user ends up pushing (NSAlertFirstButtonReturn, NSAlertSecondButtonReturn, etc. -- they're listed at the end of the NSAlert class ref). You use it in your alertDidEnd method to act upon whichever button the user pushed to dismiss the alert. That's why the alertDidEnd selector includes the returnCode.
On the other hand, when you use the runModal method in your else block, you need to explicitly call alertDidEnd and feed it the number returned when the runModal method ends -- which is when the user dismisses the alert.
Here's a revised version of your code:
int returnCode = -1;
if (displayAsSheet) {
[alert beginSheetModalForWindow:nativeWindow modalDelegate:delegate didEndSelector:#selector(alertDidEnd:returnCode:contextInfo:) contextInfo:nil];
// The selector alertDidEnd has the returnCode int. The alert will then set that code to whatever the user chooses, and will send the altered int on to alertDidEnd.
}
else {
// Here, everything stops once runModal is called, until the user dismisses the alert, at which time the runModal method returns the int representing the button the user pushed, and you assign the return to your variable "returnCode."
returnCode = [alert runModal];
[self alertDidEnd:alert returnCode:returnCode contextInfo:nil];
}
// Omit the line returning the returnCode.
Then the alertDidEnd method does something like this:
- (void)alertDidEnd:(NSAlert *)alert returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo {
switch (returnCode) {
case NSAlertFirstButtonReturn:
// Do whatever should happen when first button is pushed.
break;
case NSAlertSecondButtonReturn:
// Do whatever should happen when second button is pushed.
break;
default:
break;
}
// Unfreeze things.
[[NSApplication sharedApplication] stopModal];
}
By the way, there is a way of running a sheet and freezing the entire app, not just the window to which the sheet is attached, if that's what you want: modal tips
You're thinking about this slightly the wrong way. If your method were able to wait for the sheet to end, the app's event loop would be blocked and there would be no way for the user to interact with the UI. When you use runModal for the alert, a new run loop is created to handle the alert -- this is why nothing else can be done to the app. The point of the sheet option is to allow the user to do other things while the alert is displayed -- i.e., it expressly does not take over the event handling.
You could look into faking the sheet by attaching a child window.
You could try to set a boolean that freezes anything you want to freeze on your app (set freeze = YES) until the sheet is removed (set freeze = NO).
In the general case, you don't need to block a method : you just want some things not to happen until the user has made a choice.
For example, I have an app that uses gyroscope. It has some behaviour with, and some other behaviour without.
So I have a boolean that is used in any method that uses the gyro data to route the behaviour to the good one. My useGyro Boolean is NO when : the user is choosing what kind of hardware feature it want to enable or not, and when gyro is not available on the device.
The same thing with a mapView I have : when the user is aked by the system if it wants to be located, there is a moment where I freeze any behaviour using the user location. When he has made his choice, I change that bool value.