Open App from Context of Rich Notification iOS12 - apple-push-notifications

I have user interaction enabled on an implementation of UNNotificationContentExtension. How do I open the application once the user is done interacting with UNNotificationContentExtension / ondemand?
Note, that UNNotificationAction is specifically for the pre-programmed action, such as these. I cannot use this, if for example, I wanted the result of a .touchUpInside action on a UIButton to open the application.

To dismiss: self.extensionContext?.dismissNotificationContentExtension()
To open the application: self.extensionContext?.performNotificationDefaultAction()
(I tested this, and this worked for me. The dismiss action did not dismiss the notification entirely, just dismissed the context. The performNotificationDefaultAction dismissed the notification and opened the application. At least for me, this was not obvious from the docs and took me a bit to find.)
In your content extension, implement the optional function for UNNotificationContentExtension below in order to send a response to your application.
func didReceive(_ response: UNNotificationResponse, completionHandler completion: #escaping (UNNotificationContentExtensionResponseOption) -> Void) {
switch response.actionIdentifier {
case UNNotificationDismissActionIdentifier:
// Clearest explanation from Microsoft: https://learn.microsoft.com/en-us/dotnet/api/UserNotificationsUI.UNNotificationContentExtensionResponseOption?view=xamarin-ios-sdk-12
// Indicates that the notification interface will be dismissed, and that the content extension will handle the action.
completion(.dismiss)
// completion(.doNotDismiss)
case UNNotificationDefaultActionIdentifier:
// Default action stuff.
// Let's say the user executed default action, we want to dismiss and forward the action to the app delegate.
completion(.dismissAndForwardAction)
break
default:
break
}
To receive the response, in your app delegate, implement the function below of the UNUserNotificationCenterDelegate protocol. You can use the same switch statement as above.
#available(iOS 10.0, *)
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: #escaping () -> Void) {
// Received user info is given by response.notification.request.content.userInfo.
}
We can read the userInfo and take an action based on that in the delegate. But what if that action changes based on the user's interactions in the notification interface? For example, the content of the notification is the same, but the user pressed a button in your interface that says "Open URL" and not the other button that says "Take Action." We cannot open a URL from the interface, so we have to somehow forward this action (and not the other, standard action) to the application.
I am unsure how to do this best [1]. Please comment below if you have a solution! I am currently using UIPasteboard, which allows sharing between different applications on the Apple device. This might be one of the few solutions because your main application and the notification content extension are entirely different targets. Here are the simple CRUD actions for Pasteboard.
let pasteboard = UIPasteboard.init(name: "myApp", create: true)
C: pasteboard?.setValue("actionToTake", forKey: "setNotificationAction")
R: pasteboard?.value(forKey: "setNotificationAction") as? String
U: same as C
D: pasteboard?.setValue(nil, forKey: "setNotificationAction")
Set in the context interface; read in the AppDelegate.
[1] Could do a Cloud solution, but not ideal. Also, UserDefaults does not work (hence, Pasteboard; tried UserDefaults).

Related

How to exclude certain AppKit views from restorable NSWindow?

NSWindows can be made restorable so that their configuration is preserved between application launches.
https://developer.apple.com/documentation/appkit/nswindow/1526255-restorable
Windows should be preserved between launch cycles to maintain interface continuity for the user. During subsequent launch cycles, the system tries to recreate the window and restore its configuration to the preserved state. Configuration data is updated as needed and saved automatically by the system.
In a new macOS project, the NSWindow on a Storyboard is restorable by default:
My problem comes when embedding an NSTabViewController in the NSWindow.
The NSTabView is inheriting the window's restorable state automatically, with no added code.
This makes the selected tab persist between app launches. I don't want that. I want it to always default to index 0. If the selected tab is restored, attempting to select a tab programmatically in viewDidLoad has unexpected results.
How can I force certain AppKit UI elements to be excluded from NSWindow state restoration?
I want the Tab View to be un-restorable.
But I would like to keep other restorable benefits, such as restoring the previously-set window size.
How can single views be excluded from NSWindow state restoration?
AFAIK, you cannot exclude a certain part of the UI from being restorable. It is an either ON or OFF thing for all elements. That's why I rarely use Apple's own restorability APIs, as more often than not, they are unreliable. I always do the restoration myself to get that fine control that you need. For simpler windows, however, I let the system do the restoration.
After this preamble, and to really answer your question, I rarely use viewDidLoad() to set up any windows, because as you found out that has some nasty consequences (e.g., the window might not exist yet!). I always do that in viewWillAppear(). For that to happen, you need to set up the following:
You need to have an ivar (let's call it tabViewController) to your NSTabViewController instance in your parent NSViewController (let's call it NSViewMainController)
Override prepare(for segue: NSStoryboardSegue, sender: Any?) in NSViewMainController and set up the NSTabViewController and its NSViewController children like this:
override func prepare(for segue: NSStoryboardSegue, sender: Any?) {
// set up the tabViewController ivar
self.tabViewController = segue.destinationController as? NSTabViewController
// set up the child NSViewControllers if you need to access them via their parent (otherwise this step is not needed)
if let childControllers = tabViewController?.children {
for controller in childControllers {
if let controller = controller as? NSViewController1 {
childController1 = controller
}
else if let controller = controller as? NSViewController2 {
childController2 = controller
}
else if let controller = controller as? NSViewController3 {
childController3 = controller
}
}
}
}
Override viewWillAppear() of NSViewMainController and then set up the desired tabView:
guard let controller = tabViewController else { return }
controller.selectedTabViewItemIndex = 0
Major caveat: Beware of viewWillAppear(), though... Unlike viewDidLoad(), this override can be called multiple times, and thus you need to take that into account in your code and react appropriately.
The key to state restoration is the NSResponder method restoreStateWithCoder:
This method is part of the window restoration system and is called at launch time to restore the visual state of your responder object. The default implementation does nothing but specific subclasses (such as NSView and NSWindow) override it and save important state information. Therefore, if you override this method, you should always call super at some point in your implementation.
https://developer.apple.com/documentation/appkit/nsresponder/1526253-restorestate
So, to not restore a certain control, make this method a no-op.
It says that you "should always call super", but that restores the window state. So if you don't want window restoration, don't call super.
In the case of a Tab View, it evidently must be done in the NSTabView (subclass) itself. In other views, overriding this method on the View Controller may work.
class SomeTabView: NSTabView {
override func restoreState(with coder: NSCoder) {
// Do NOT restore state
}
}

Detecting When App Enters Full Screen Mode (Swift/Mac)

I'm hoping this is a simple question, but I am trying to discern a way to figure out when a user has selected to enter Full Screen mode in an app. Effectively, I have a table in a Cocoa app that looks rather silly when the app enters full-screen mode. I would like to, programmatically, adjust the height of my table rows once the app enters full screen mode, but I cannot seem to figure out how to do so.
I recognize the need to use windowWillEnterFullScreen: and windowDidEnterFullScreen:, or find a way for my Window to conform to my App Delegate file, though I'm struggling to figure this out. Are there any resources that could be provided that may be able to point in the right direction?
Thank you!
Edit: Here's what I've tried to do;
AppDelegate.swift
import Cocoa
#NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
var window: NSWindow!
func windowDidResize (notification: NSNotification) {
window.delegate = self
print("resized")
}
func applicationDidFinishLaunching(aNotification: NSNotification) {
// Insert code here to initialize your application
}
func applicationWillTerminate(aNotification: NSNotification) {
// Insert code here to tear down your application
}
}
windowWillEnterFullScreen: and windowDidEnterFullScreen: are NSWindowDelegate methods — to be able to use them, you just need to be the NSWindow's delegate. Your app delegate object or any other object could serve this purpose.
If you want to use custom animations during the transition, there are some other delegate methods such as window:startCustomAnimationToEnterFullScreenOnScreen:withDuration: that you could use.
You can also check window.styleMask & NSFullScreenWindowMask != 0 to check whether the window is currently fullscreen.

How to enable undo menu item when a sheet is presented

I am creating a document based core data OSX app using storyboards. Undo and redo works fine until I present a View Controller in a sheet with a segue. Once the sheet is presented, the undo / redo buttons are grayed out.
While searching for a possible solution, I came across this article in which they say that I have to supply an undo manager to my window using the
"windowWillReturnUndoManager:" delegate method. So I implemented this method in the sourceController of my segue, and set that controller as the delegate for the window of the destinationController in the prepareForSegue method like this:
override func prepareForSegue(segue: NSStoryboardSegue, sender: AnyObject?) {
super.prepareForSegue(segue, sender: sender)
(segue.destinationController as NSViewController).view.window?.delegate = self
}
func windowWillReturnUndoManager(window: NSWindow) -> NSUndoManager? {
println(undoManager)
return undoManager
}
But the undo and redo buttons are still grayed out when I open the sheet. Note that when change the segue style to popover, the undo/redo are working perfectly. How can I resolve this?
I had the same problem and am posting here in case it might help someone. My solution was to obtain the undo manager for the window to which the sheet view controller is attached:
let undoManager = self.view.window?.firstResponder?.undoManager
self in this context is the view controller for the sheet and not the parent view controller for the sheet. Therefore, the assignment would take place within the view controller for the sheet.

NSTextField - notifications when individual keys are pressed

I am making an app that will add sound to keypresses as the user types in an NSTextField. I need to capture keystrokes and know what each individual keypress is (like "d" or "space" or "6"). The app depends on this. There is no other way around it.
Each window is an NSDocument File Owner, and it has a single NSTextField in it, which is where the document data is parsed, and the user will type.
After hours of parsing the Internet for answers and hacking away at code, the four most commonly repeated answers are:
"that is not how things work, here is (irrelevant answer)"
"you are new to Cocoa, that is a bad idea, use control:textView:doCommandSelector:" that doesn't give me individual keys, and some keys need their own unique sound trigger.
"use controlTextDidChange: or textView:shouldChangeTextInRange:replaceString:" controlTextDidChange doesn't give me individual keys, and the second one only works for textViews or UIKit.
People get confused and answer with recommendations for UIKit instead of AppKit, which is iOS-only.
The weird thing is that if I subclass NSTextField, it receives -keyUp. I don't know where -keyDown is going.
So my ultimate question is: can you tell me some kind of step-by-step way to actually capture the keyDown that is sent to NSTextField? Even if it's a hack. Even if it's a terrible idea.
I would love to solve this problem! I am very grateful for your reading.
controlTextDidChange is quite a good solution, but don't forget this 2 important things:
Set the delegate binding of the textField to the object where you define the controlTextDidChange method. Commonly, in document based apps it is the window controller, otherwise your app delegate.
Set the textField's control to "continous" in the attribute inspector section
If you miss those points, you will have no result.
This is a pretty old question, but as I was trying to implement a NSTextField that could react to keyDown so that I could create a hotkey preferences control I found I wanted the answer to this question.
Unfortunately this is a pretty non-standard use and I didn't find any places that had a direct answer, but I've come up with something that works after digging through the documentation (albeit in Swift 4) and I wanted to post it here in case it helps someone else with a non-standard use case.
This is largely based off of the information gleaned from the Cocoa Text Architecture Guide
There are three components to my solution:
Creating your NSWindowController and setting a NSWindowDelegate on your NSWindow:
guard let windowController = storyboard.instanciateController(withIdentifier:NSStoryboard.SceneIdentifier("SomeSceneIdentifier")) as? NSWindowController else {
fatalError("Error creating window controller");
}
if let viewController = windowController.contentViewController as? MyViewController {
windowController.window?.delegate=viewController;
}
Your NSWindowDelegate
class MyViewController: NSViewController, NSWindowDelegate {
// The TextField you want to capture keyDown on
var hotKeyTextField:NSTextField!;
// Your custom TextView which will handle keyDown
var hotKeySelectionFieldEditor:HotKeySelectionTextView = HotKeySelectionTextView();
func windowWillReturnFieldEditor(_ sender: NSWindow, to client: Any?) -> Any? {
// If the client (NSTextField) requesting the field editor is the one you want to capture key events on, return the custom field editor. Otherwise, return nil and get the default field editor.
if let textField = client as? NSTextField, textField.identifier == hotKeyTextField.identifier {
return hotKeySelectionFieldEditor;
}
return nil;
}
}
Your custom TextView where you handle keyDown
class HotKeySelectionTextView: NSTextView {
public override func keyDown(with event: NSEvent) {
// Here you can capture the key presses and perhaps save state or communicate back to the ViewController with a delegate pattern if you prefer.
}
}
I fully admit that this feels like a workaround somewhat, but as I am experimenting with Swift at the moment and not quite up to speed with all of the best practices yet I can't make an authoritative claim as to the "Swift-i-ness" of this solution, only that it does allow a NSTextField to capture keyDown events indirectly while maintaining the rest of the NSTextField functionality.
Try like this if you print nslog you will get individual character record for example you pressd "A" you will get the same in console:-
-(void)controlTextDidChange:(NSNotification*)obj
{
NSLog(#"%#",[yourTextfield stringValue]);
}
Also, not sure this is only your requirement.
Text editing for an NSTextField is handled by an NSTextView provided by the window, called the field editor. See the NSWindow method fieldEditor:forObject: and the NSWindowDelegate method windowWillReturnFieldEditor:toObject:. I suppose you could use one of these to provide your own subclassed NSTextView as the field editor. Or, could you simply use NSTextView instead of NSTextField?

NSMenuItem KeyEquivalent " "(space) bug

I want to set key equivalent " "(space) without any modifiers for NSMenuItem (in App Main Menu).
As follows from documentation:
For example, in an application that plays media, the Play command may be mapped to just “ ” (space), without the command key. You can do this with the following code:
[menuItem setKeyEquivalent:#" "];
[menuItem setKeyEquivalentModifierMask:0];
Key Equivalent sets successfully, but it don't work. When I press "Space" key without modifiers nothing happens, but it's works when i press "Space" with "Fn" modifier key.
I need to use "Space" without modifiers. Any help please!
This is a tricky question. Like many answers suggest, intercepting the event at the application or window level is a solid way to force the menu item to work. At the same time it is likely to break other things, for example, if you have a focused NSTextField or NSButton you'd want them to consume the event, not the menu item. This might also fail if the user redefines the key equivalent for that menu item in system preferences, i.e., changes Space to P.
The fact that you're using the space key equivalent with the menu item makes things even trickier. Space is one of the special UI event characters, along with the arrow keys and a few others, that the AppKit treats differently and in certain cases will consume before it propagates up to the main menu.
So, there are two things to keep in mind. First, is the standard responder chain:
NSApplication.sendEvent sends event to the key window.
Key window receives the event in NSWindow.sendEvent, determines if it is a key event and invokes performKeyEquivalent on self.
performKeyEquivalent sends it to the current window's firstResponder.
If the responder doesn't consume it, the event gets recursively sent upwards to the nextResponder.
performKeyEquivalent returns true if one of the responders consumes the event, false otherwise.
Now, the second and tricky part, if the event doesn't get consumed (that is when performKeyEquivalent returns false) the window will try to process it as a special keyboard UI event – this is briefly mentioned in Cocoa Event Handling Guide:
The Cocoa event-dispatch architecture treats certain key events as commands to move control focus to a different user-interface object in a window, to simulate a mouse click on an object, to dismiss modal windows, and to make selections in objects that allow selections. This capability is called keyboard interface control. Most of the user-interface objects involved in keyboard interface control are NSControl objects, but objects that aren’t controls can participate as well.
The way this part works is pretty straightforward:
The window converts the key event in a corresponding action (selector).
It checks with the first responder if it respondsToSelector and invokes it.
If the action was invoked the event gets treated as consumed and the event propagation stops.
So, with all that in mind, you must ensure two things:
The responder chain is correctly set up.
Responders consumes only what they need and propagate events otherwise.
The first point rarely gives troubles. The second one, and this is what happens in your example, needs taking care of – the AVPlayer would typically be the first responder and consume the space key event, as well as a few others. To make this work you need to override keyUp and keyDown methods to propagate the event up the responder chain as would happen in the default NSView implementation.
// All player keyboard gestures are disabled.
override func keyDown(with event: NSEvent) {
self.nextResponder?.keyDown(with: event)
}
// All player keyboard gestures are disabled.
override func keyUp(with event: NSEvent) {
self.nextResponder?.keyUp(with: event)
}
The above forwards the event up the responder chain and it will eventually be received by main menu. There's one gotcha, if first responder is a control, like NSButton or any custom NSControl-inheriting object, it WILL consume the event. Typically you do want this to happen, but if not, for example when implementing custom controls, you can override respondsToSelector:
override func responds(to selector: Selector!) -> Bool {
if selector == #selector(performClick(_:)) { return false }
return super.responds(to: selector)
}
This will prevent the window from consuming the keyboard UI event, so the main menu can receive it instead. However, if you want to intercept ALL keyboard UI events, including when the first responder is able to consume it, you do want to override your window's or application's performKeyEquivalent, but without duplicating it as other answers suggest:
override func performKeyEquivalent(with event: NSEvent) -> Bool {
// Attempt to perform the key equivalent on the main menu first.
if NSApplication.shared.mainMenu?.performKeyEquivalent(with: event) == true { return true }
// Continue with the standard implementation if it doesn't succeed.
return super.performKeyEquivalent(with: event)
}
If you invoke performKeyEquivalent on the main menu without checking for result you might end up invoking it twice – first, manually, and second, automatically from the super implementation, if the event doesn't get consumed by the responder chain. This would be the case when AVPlayer is the first responder and keyDown and keyUp methods not overwritten.
P.S. Snippets are Swift 4, but the idea is the same! ✌️
P.P.S. There's a brilliant WWDC 2010 Session 145 – Key Event Handling in Cocoa Applications that covers this subject in depth with excellent examples. WWDC 2010-11 is no longer listed on Apple Developer Portal but the full session list can be found here.
I had the same problem. I haven't investigated very hard, but as far as I can tell, the spacebar doesn't "look" like a keyboard shortcut to Cocoa so it gets routed to -insertText:. My solution was to subclass the NSWindow, catch it as it goes up the responder chain (presumably you could subclass NSApp instead), and send it off to the menu system explicitly:
- (void)insertText:(id)insertString
{
if ([insertString isEqual:#" "]) {
NSEvent *fakeEvent = [NSEvent keyEventWithType:NSKeyDown
location:[self mouseLocationOutsideOfEventStream]
modifierFlags:0
timestamp:[[NSProcessInfo processInfo] systemUptime]
windowNumber:self.windowNumber
context:[NSGraphicsContext currentContext]
characters:#" "
charactersIgnoringModifiers:#" "
isARepeat:NO
keyCode:49];
[[NSApp mainMenu] performKeyEquivalent:fakeEvent];
} else {
[super insertText:insertString];
}
}
I have just been experiencing the same problem with a twist...
The spacebar key equivalent works fine in my app while the NSMenuItem's linked IBAction is located in the App Delegate.
If I move the IBAction into a dedicated controller it fails. All other menu item key equivalents continue to work but the spacebar does not respond (it is ok with a modifier key, but unmodified #" " will not work).
I have tried various workarounds, like linking directly to the controller vs. linking via the responder chain, to no avail. I tried the code way:
[menuItem setKeyEquivalent:#" "];
[menuItem setKeyEquivalentModifierMask:0];
and the Interface Builder way, the behaviour is the same
I have tried subclassing NSWindow, as per Justin's answer, but so far have failed to get that to work.
So for now I have surrendered and relocated this one IBAction to the App Delegate where it works. I don't regard this as a solution, just making do... perhaps it's a bug, or (more likely) I just don't understand event messaging and the responder chain well enough.
Up this post because i need to use space too but no of those solutions work for me.
So, I subclass NSApplication and use the sendEvent: selector with the justin k solution :
- (void)sendEvent:(NSEvent *)anEvent
{
[super sendEvent:anEvent];
switch ([anEvent type]) {
case NSKeyDown:
if (([anEvent keyCode] == 49) && (![anEvent isARepeat])) {
NSPoint pt; pt.x = pt.y = 0;
NSEvent *fakeEvent = [NSEvent keyEventWithType:NSKeyDown
location:pt
modifierFlags:0
timestamp:[[NSProcessInfo processInfo] systemUptime]
windowNumber: 0 // self.windowNumber
context:[NSGraphicsContext currentContext]
characters:#" "
charactersIgnoringModifiers:#" "
isARepeat:NO
keyCode:49];
[[NSApp mainMenu] performKeyEquivalent:fakeEvent];
}
break;
default:
break;
}
}
Hope it will help
Quick Swift 4-5 method:
In view controller:
// Capture space and call main menu
override func keyDown(with event: NSEvent) {
if event.keyCode == 49 && !event.isARepeat{
NSApp.mainMenu?.performKeyEquivalent(with: event)
}
super.keyDown(with: event)
}

Resources