I have a WKWebView with a custom implementation of undo and redo. I would like to be able to know when the system undo / redo are triggered (via gestures or via tapping on the keyboard assistant button in iPadOS) so that I can use my custom implementation.
Is there a public API to do that?
There are multiple ways. On the iOS native side there is no public API available to take over full control AFAIK, but you can listen to UNDO notifications like this to get to know about them:
[NSNotificationCenter.defaultCenter addObserverForName:NSUndoManagerWillUndoChangeNotification object:nil queue:NSOperationQueue.mainQueue usingBlock:^(NSNotification * _Nonnull note) {
NSLog(#"Undo Notification: %#", note);
}];
You will then see that the NSUndoManager responsible for that lives in WKContentView. To get there only swizzling helps with the known risks...
But there is another way that works in WebKit based browser views (as WKWebView is) and this is to listen to beforeinput events. For example for a contenteditable element you can add the following listener:
editor.addEventListener('beforeinput', event => {
if (event.inputType === 'historyUndo') {
console.log('undo')
event.preventDefault()
}
if (event.inputType === 'historyRedo') {
console.log('redo')
event.preventDefault()
}
console.log('some other inputType', event.inputType)
})
This idea was was taken from this discussion: https://discuss.prosemirror.net/t/native-undo-history/1823/3?u=holtwick
Here is a JSFiddle to test: https://jsfiddle.net/perenzo/bhztrgw3/4/
See also the corresponding TipTap plugin: https://github.com/scrumpy/tiptap/issues/468
Related
In my ionic application for IOS I am listening to a window event generated by a cordova plugin.
Here is the code that I use for listen to the event and perform an action.
window.addEventListener('event', (event) => {
...
console.log("event received");
doSomething();
});
doSomething(){console.log("perform an action");}
Everything work and I am able to receive the event until I open the IOS Control Center (swipe up from the bottom). After I close the Control Center again I can see that the event is logged ("event received") but the function doSomething() is never called.
Someone encountered a similar situation?
Since the event is generated outside angular, I needed to call ngZone.run in order to let angular know that something happend and so trigger the change. I thid it this way
constructor(private zone: NgZone) {}
ngOnInit(){
window.addEventListener('event', (event) => {
this.ngZone.run(() => {
do stuff;
});
}
Issue: Different Behavior In 3 Different Contexts
Ok so Ok, in iOS it seems three different things can happen regarding Push Notifications:
When a Push Notification is received when the app is not in the foreground
something shows up in Notification Center
if the app is opened by tapping the notification, either AppDelegate.DidReceiveRemoteNotification(...) or AppDelegate.ReceivedRemoteNotification(...) is called, apparently depending on which one is implemented (??).
if the app is opened without tapping the notification, only AppDelegate.WillEnterForeground(...), is called, without any explicit mention of the notification, and nothing else happens to acknowledge that a notification was received.
When a Push Notification is received when the app is in the foreground it causes the UNUserNotificationCenterDelegate, if there is one, to execute UNUserNotificationCenterDelegate.WillPresentNotification(...).
Approach: Routing To One Method From All Contexts
So to cover all bases with Push I need to implement something in all three methods: AppDelegate.DidReceiveRemoteNotification(...) / AppDelegate.ReceivedRemoteNotification(...), AppDelegate.WillEnterForeground(...), and UNUserNotificationCenterDelegate .WillPresentNotification(...).
Here are some stubs to show my approach to all this.
First, I created a custom UNUserNotificationCenterDelegate, with a Shared static member:
public class IncomingNotificationHandler : UNUserNotificationCenterDelegate
{
public static IncomingNotificationHandler Shared = new IncomingNotificationHandler();
...
}
Second, inside that class I made a handler that I can route to in every case (again, this is just a stub for debugging purposes):
//sets all parameters to null by default, so it can be called from methods
//that don't know anything about notifications:
public void HandleNotificationsIfAny(UIApplication application = null,
NSDictionary userInfo = null,
Action<UIBackgroundFetchResult> completionHandler = null)
{
//checks if userInfo is null, and logs its conclusions about that:
if (userInfo == null)
{
//In the null case, we can get pending notifications from
//UNUserNotificationCenter:
UNNotification[] pendingNotifications = new UNNotification[] { };
UNUserNotificationCenter.Current.GetDeliveredNotifications(returnedValue => pendingNotifications = returnedValue);
//Then we log the number of pending notifications:
Debug.WriteLine("IncomingNotificationHandler: HandleNotificationsIfAny(...): delivered notification count: " + pendingNotifications.Length);
//And make note of where this was probably called from:
Debug.WriteLine("IncomingNotificationHandler: HandleNotificationsIfAny(...): may have been called from this.WillPresentNotification(...) OR AppDelegate.WillEnterForeground(...)");
return;
});
}
else
{
//In the non-null case, we log the userInfo
Debug.WriteLine("IncomingNotificationHandler: HandleNotificationsIfAny(...): just got info: " + userInfo);
//And make note of where this was probably called from:
Debug.WriteLine("IncomingNotificationHandler: HandleNotificationsIfAny(...): may have been called from AppDelegate.DidReceiveRemoteNotification(...)");
}
}
Third, inside the same class, I implemented the single method that's required by UNUserNotificationCenterDelegate, and I routed to the handler from it:
public override void WillPresentNotification(UNUserNotificationCenter center, UNNotification notification, Action<UNNotificationPresentationOptions> completionHandler)
{
HandleNotificationsIfAny();
}
Fourth, and last, inside AppDelegate, I routed to the same handler from both relevant methods:
//I prefer using DidReceiveRemoteNotification because in my experience
//the other one is sometimes not reliable:
public override void DidReceiveRemoteNotification(UIApplication application,
NSDictionary userInfo,
Action<UIBackgroundFetchResult> completionHandler)
{
//Simply passing on all the parameters called in this method:
IncomingNotificationHandler.Shared.HandleNotificationsIfAny(application, userInfo, completionHandler);
}
//WillEnterForeground also calls the handler without any parameters
//because it doesn't automatically know anything about notifications:
public override void WillEnterForeground(UIApplication application)
{
IncomingNotificationHandler.Shared.HandleNotificationsIfAny();
}
With that, as it stands, I think I'm handling a notification event in the same way no matter how my app is alerted about it, and even when it's not alerted at all.
Does anyone know if I now have it covered, or if there's some other cases I need to handle?
For the first scenario: AppDelegate.ReceivedRemoteNotification
It reflects the objective c method: application:didReceiveRemoteNotification:, but this event has been deprecated since iOS 10: https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1623117-application?language=objc. So I think there's no need to handle this event.
For the second scenario: AppDelegate.DidReceiveRemoteNotification
You can still utilize it to handle notifications now if you haven't implemented UNUserNotificationCenter and please notice it is only valid after iOS 7+. Moreover, this event will be triggered when app is on the foreground and if your app is on the background, this event only fires when the user clicks the notification to open your application. And there's no way to access the notification's information if the user clicks the icon to open the app.
I don't think handling AppDelegate.WillEnterForeground is a good approach, as it will be called each time the app resumes from background to foreground even though there are no notifications.
For the scenario: UNUserNotificationCenterDelegate
You could only use this feature after iOS 10. Once you have implemented it on the device iOS 10+, DidReceiveRemoteNotification and ReceivedRemoteNotification will never be triggered. WillPresentNotification will be called when app is on the foreground. DidReceiveNotificationResponse will be fired when the app is on the background and user clicks notifications to open it.
As a conclusion, if you want to easily handle the notification AppDelegate.DidReceiveRemoteNotification is enough. If you want to consume the new features of UNUserNotificationCenter, AppDelegate.DidReceiveRemoteNotification and UNUserNotificationCenter should be both involved. The prior one for the iOS 7+ devices and the later one for iOS 10+ devices.
Update:
For iOS 10+, you could use UNUserNotificationCenter.Current.GetDeliveredNotifications to obtain the notifications that are still displayed in Notification Center. And if you only want to support iOS version 10 and later. I think UNUserNotificationCenter is enough, there's no need to implement AppDelegate.DidReceiveRemoteNotification(...) or AppDelegate.ReceivedRemoteNotification(...).
If the app is on background / killed state and the user clicks notification to
open the app, DidReceiveNotificationResponse will be called.
If the
user clicks icon to open your app and the app is killed you should
place your logic code in FinishedLaunching.
If the user clicks icon
to open your app and app is on background, you can handle
WillEnterForeground as you did before.
If the app is on foreground,
handle WillPresentNotification.
I have a native iOS module that needs to fire events periodically. These events work perfectly... sometimes. Randomly, they just dont fire.
In Obj-C, The button fires an event on the NSNotificationCenter defaultCenter, with a TiBlob image. My module class listens for this notification, and calls the following function:
- (void) imageCaptured:(NSNotification *) notification
{
NSLog(#"[INFO] module notified of image capture event");
if ([self _hasListeners:IMAGECAPTUREDEVENT])
[self fireEvent:IMAGECAPTUREDEVENT withObject:notification.userInfo];
else
NSLog(#"[INFO] if an image is captured, and there is no one there to listen for it, does an event get fired?");
}
And then in JS I have an eventlistener like so:
mymodule.addEventListener('imageCaptured', function(e){
Ti.API.info('image capture event caught in JS.);
});
This does work some of the time. I can even see the Ti.blob object returned if I add that into the listener, but other times... I see the "[INFO] module notified of image capture event" log, and then nothing. No errors. No clever line about there being no listeners. And no "image capture event caught in JS."
AS some one porting code from actionscript to IOS, We have a lot of custom components that follow the event dispatching mechanism in Flash/Actionscript:
E.g. dispatcher:
dispatchEvent(new CustomEvent(CustomEvent.DRAG_DROP));
Consumer:
dispatcher.addEventListener(CustomEvent.DRAG_DROP, actionHandler);
private function actionHandler(event:CustomEvent):void {
trace("actionHandler: " + event);
}
I know of NSNotificationCenter, KVO pattern, action-target, but none seem to be an exact match?
Where would I define CustomEvent? CustomEvent.DRAG_DROP? and how would the consumer listen for the event? How would a consumer know of all the events that a dispatcher can dispatch? I do not wish to use a delegate because there could be multiple consumers.
The closes way I know is selectors ...
// store event handler
SEL targetHandler;
// firing an event
[targetHandler performSelector:targetHandler withObject:eventObj];
// event handler in the listening class
- (void) onStuffHappened: (Event*) event
{
}
that's obviously a quick thought, I would extend NSObject and store handlers in NSMutableArray then run performSelector on all the stored handlers ... something like that
or you can use delegates for a cleaner way.
Generally this is done with a list of delegates. If you want a multiple consumers, define a protocol (just like you would for a delegate), and then create an array of those objects. When you want to communicate with all of the listeners iterate through the list of listeners sending the event to each one.
Before I deem my weak-long custom re-implementation of UITextView (using an UIWebView in designMode) useless, is there any way to handle/cancel javaScript onKeyUp, etc. events?
AFAIK there is only messaging via -shouldLoadRequest: & stringByEvaluatingScriptWithString:. However, these calls are asynchronous and the javaScript event handler has already exited it's function by the time stringByEvaluatingScriptWithString: is performed thus event cancellation methods do not work.
If not for this capability, implementing shouldReplaceCharactersInString: seems impossible. :(
Maybe you can check my open source implementation of a "safari like" browser :
https://github.com/sylverb/CIALBrowser
In this one, I did reimplement the long tap handling, and to disable the standard one, I use this :
- (void) webViewDidFinishLoad:(UIWebView *) sender
{
// Disable the defaut actionSheet when doing a long press
[webView stringByEvaluatingJavaScriptFromString:#"document.body.style.webkitTouchCallout='none';"];
[webView stringByEvaluatingJavaScriptFromString:#"document.documentElement.style.webkitTouchCallout='none';"];
}