How to avoid asynchronous nature of scrollToVisibleRect causing intermittent timing issues - cocoa

I am writing a Cocoa UI and am calling NSView's scrollRectToVisible repeatedly in a short space of time as a result of the user holding down a certain key (and hence repeatedly firing events on the main queue).
Through logging I can see that successive keyDown() events are firing prior to the scrollRectToVisible having completed its prior changing of the visibleRect. This is resulting in the subsequent scrollRectToVisible being called with incorrect inputs (namely the wrong starting visibleRect) and is leading to non-sensical UI behaviour. This happens about 50% of the time which I guess is to be expected dealing with an asynchronous problem.
How can I address this?
One way I can think of is by somehow turning scrollRectToVisible into a synchronous call. The problem is that it is an AppKit API and the method returns immediately with a boolean indicating whether it is going to scroll or not.

Try putting the code in the view that responds to the -keyDown:.
This example controls the scrolling with a timer instead of the repeated keyDowns from holding the key.
I couldn't get the UI unwanted behavior, so I don't know if this will help.
- (BOOL)acceptsFirstResponder {
return YES;
}
- (BOOL)canBecomeKeyView {
return YES;
}
- (void)keyDown:(NSEvent *)theEvent {
if (timer == nil) {
timer = [NSTimer scheduledTimerWithTimeInterval:0.01
target:self
selector:#selector(doScroll:)
userInfo:nil
repeats:YES];
}
NSLog(#"event %# with Timer%#", theEvent, timer);
}
- (void)keyUp:(NSEvent *)theEvent {
[timer invalidate];
timer = nil;
NSLog(#"keyUp timer:%#", timer);
NSLog(#"event %#", theEvent);
}
- (void)doScroll:theTimer {
NSEvent * current = [NSApp currentEvent]; //use this to parse
NSLog(#"current press %#", current);
[self scrollRectToVisible:NSMakeRect(self.visibleRect.origin.x + 10,
self.visibleRect.origin.y + 10,
self.bounds.size.width,
self.bounds.size.height)];
}

Related

Inject keyboard event into NSRunningApplication immediately after foregrounding it

I am trying to bring to foreground a NSRunningApplication* instance, and inject a keyboard event.
NSRunningApplication* app = ...;
[app activateWithOptions: 0];
inject_keystrokes();
... fails to inject keyboard events, but:
NSRunningApplication* app = ...;
[app activateWithOptions: 0];
dispatch_time_t _100ms = dispatch_time( DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC) );
dispatch_after(
_100ms,
dispatch_get_main_queue(),
^{ inject_keystrokes(); }
);
... succeeds.
I imagine it takes a certain amount of time for the window to render in the foreground, and maybe this happens on a separate thread, and this explains the injection failure.
However this is a very ugly solution. It relies on an arbitrary time interval.
It would be much cleaner to somehow wait for the window to complete foregrounding.
Is there any way of doing this?
PS inject_keystrokes() uses CGEventPost(kCGHIDEventTap, someCGEvent)
PPS Refs:
- Virtual keypress goes to wrong application
- Send NSEvent to background app
- http://advinprog.blogspot.com/2008/06/so-you-want-to-post-keyboard-event-in.html
Adding an observer for the KVO property isActive on NSRunningApplication works for me.
for (NSRunningApplication* ra in [[NSWorkspace sharedWorkspace] runningApplications])
{
if ([ra.bundleIdentifier isEqualToString:#"com.apple.TextEdit"])
{
[ra addObserver:self forKeyPath:#"isActive" options:0 context:ra];
[ra retain];
[ra activateWithOptions:0];
}
}
// ...
- (void)observeValueForKeyPath:(NSString*)keyPath ofObject:(id)object change:(NSDictionary*)change context:(void*)context
{
if ([keyPath isEqualToString:#"isActive"])
{
NSRunningApplication* ra = (NSRunningApplication*) context;
[ra removeObserver:self forKeyPath:#"isActive"];
[ra release];
inject_keystrokes();
}
}
Note that I manually retain and then release the NSRunningApplication to keep its reference alive, since I'm not keeping it in a property or ivar. You have to be careful that the reference doesn't get dropped with the observer still attached.

objective c WatchKit WKInterfaceController openParentApplication call blocks indefinitely

I'm using the following code to "simply" determine the application state of the parent application from my watch app:
WatchKit Extension:
[WKInterfaceController openParentApplication:[NSDictionary dictionary] reply:^(NSDictionary *replyInfo, NSError *error)
{
UIApplicationState appState = UIApplicationStateBackground;
if(nil != replyInfo)
appState = (UIApplicationState)[((NSNumber*)[replyInfo objectForKey:kAppStateKey]) integerValue];
//handle app state
}];
Main App:
- (void)application:(UIApplication *)application handleWatchKitExtensionRequest:(NSDictionary *)userInfo reply:(void (^)(NSDictionary *replyInfo))reply
{
__block UIBackgroundTaskIdentifier realBackgroundTask;
realBackgroundTask = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
reply([NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithInt:[[UIApplication sharedApplication] applicationState]], kAppStateKey, nil]);
[[UIApplication sharedApplication] endBackgroundTask:realBackgroundTask];
}];
reply([NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithInt:[[UIApplication sharedApplication] applicationState]], kAppStateKey, nil]);
[[UIApplication sharedApplication] endBackgroundTask:realBackgroundTask];
}
When the app is in the foreground this works 100% of the time. When the app is "minimized" or "terminated" this maybe works 50% of the time (maybe less). When it doesn't work it appears to be blocking indefinitely. If after 1 minute, for example, I launch the parent app, the call (openParentApplication) immediately returns with the state "UIApplicationStateBackground" (the state it was before I launched the app as clearly the app isn't in the background state if I launched it).
BTW: I'm testing with real hardware.
What am I doing wrong? Why is iOS putting my main app to sleep immediately after receiving the call even though I create a background task? This is a complete show-stopper.
Any thoughts or suggestions would be greatly appreciated!!
After some research it looks to be a known issue. For example, the following link identifies this issue and provides a solution:
http://www.fiveminutewatchkit.com/blog/2015/3/11/one-weird-trick-to-fix-openparentapplicationreply
However, this solution did not work for me. As a result I implemented the following solution (its a little sloppy, but this is intentional to help condense the solution):
//start the timeout timer
timeoutTimer = [NSTimer scheduledTimerWithTimeInterval:kTimeOutTime target:self selector:#selector(onTimeout) userInfo:nil repeats:NO];
//make the call
messageSent = [WKInterfaceController openParentApplication:[NSDictionary dictionary] reply:^(NSDictionary *replyInfo, NSError *error)
{
if(nil != _stateDelegate)
{
UIApplicationState appState = UIApplicationStateBackground;
if(nil != replyInfo)
appState = (UIApplicationState)[((NSNumber*)[replyInfo objectForKey:kAppStateKey]) integerValue];
[_stateDelegate onOperationComplete:self timeout:false applicationState:appState];
_stateDelegate = nil;
}
}];
//if the message wasn't sent, then this ends now
if(!messageSent)
{
if(nil != _stateDelegate)
{
//just report that the main application is inactive
[_stateDelegate onOperationComplete:self timeout:false applicationState:UIApplicationStateInactive];
}
_stateDelegate = nil;
}
-(void)onTimeout
{
timeoutTimer = nil;
if(nil != _stateDelegate)
{
[_stateDelegate onOperationComplete:self timeout:true applicationState:UIApplicationStateInactive];
}
_stateDelegate = nil;
}
In a nutshell, if the timer fires before I hear back from the main app I will basically assume that the main app has been put to sleep. Keep in mind that all pending calls will succeed at some point (e.g. app state is restored to active) and, thus, you will need to handle this scenario (if necessary).

Run loop doesn't proceed events

I'm testing with run loops in standard (created by XCode) App. My App has 2 buttons:
Start Loop - starts runloop in some mode (see code below);
Stop Loop - change self.stop flag to stop runloop.
`
- (IBAction)stopLoop:(id)sender
{
self.stop = YES;
}
- (IBAction)startLoop:(id)sender
{
self.stop = NO;
do
{
[[NSRunLoop currentRunLoop] runMode:runLoopMode beforeDate:runLoopLimitDate];
if (self.stop)
{
break;
}
} while (YES);
}
`
where:
1. runLoopMode is one of the predefined modes (I try each, default, event tracking, modal, connection).
2. runLoopLimitDate [NSDate distantFuture], or [NSDate distantPast], or close feature.
3. self.stop flag is installed in other method, which called by button.
That's all, my App hasn't any other code.
AFAIU, runloop mode is a set of event sources. So, if I run runloop in some mode, runloop will be proceed those event sources, whose are associated with this mode.
By default Cocoa runs runloop in default mode and all events are proceeds greatly. But when user press startLoop button, my App is freezing: .
startLoop method is never break this infinity cycle. Application doesn't send any event to me, therefore UI freezing and user can't press stopLoop button. The same problem if I run Core Foundation counterparts.
But, when I try to receive events through NSApplication (of NSWindow) method nextEventMatchingMask:untilDate:inMode:dequeue: and pass the same mode, I receive UI events.
- (IBAction)startLoop:(id)sender
{
self.stop = NO;
do
{
NSEvent *event = [NSApp nextEventMatchingMask:NSAnyEventMask untilDate:nil inMode:NSEventTrackingRunLoopMode dequeue:YES];
if (event == nil)
{
break;
}
[NSApp sendEvent:event];
if (self.stop)
{
break;
}
} while (YES);
}
There are question: "Why if I run default run loop mode, or some other, in this way, I can't receive events?"
Thanks for your advice.
You are assuming that running the NSRunLoop in the NSDefaultRunMode processes user input events such as key presses or mouse clicks. I don't think that's the case.
NSApplication fetches events from the event with nextEventMatchingMask:untilDate:inMode:dequeue:. By running the run loop like that your not really fetching any event of the event queue.
Cloud you try something like this:
- (IBAction)startLoop:(id)sender
{
do
{
NSEvent *event = [NSApp nextEventMatchingMask:NSAnyEventMask];
[NSApp sendEvent:event];
if (self.stop)
{
break;
}
} while (YES);
}
(Haven't tested this).
What happens if you substitute the following code in your app in place of the -startLoop: method?
- (NSString *) debugLogRunLoopInfo(BOOL didRun)
{
NSLog (#"didRun? %#, runMode: %#, dateNow: %#, limitDate: %#",
didRun ? #"YES" : #"NO",
runLoopMode,
[NSDate date],
limitDate);
}
- (IBAction)startLoop:(id)sender
{
BOOL didRun = NO;
do
{
didRun = [[NSRunLoop currentRunLoop] runMode:runLoopMode beforeDate:runLoopLimitDate];
[self debugLogRunLoopInfo:didRun];
if (self.stop)
{
break;
}
} while (YES);
}
The -[NSRunLoop runUntilDate:] method spins the runloop in NSDefaultRunLoopMode. Since you're wanting to experiment with runloop modes, you could try the code below.
I've implemented a -myRunLoopUntilDate:runMode: method that does what runUntilDate: is documented to do, but allows you to specify a runloop mode.
All of this is compiled in my text editor (i.e. not compiled at all), so caveat emptor.
- (NSString *) debugLogRunLoopInfo(BOOL didRun)
{
NSLog (#"didRun? %#, runMode: %#, dateNow: %#, limitDate: %#",
didRun ? #"YES" : #"NO",
runLoopMode,
[NSDate date],
limitDate);
}
- (void) myRunLoopUntilDate:(NSDate *)limitDate runMode:(NSString *)runLoopMode
{
BOOL didRun = NO;
do {
didRun = [[NSRunLoop currentRunLoop] runMode:runLoopMode beforeDate:limitDate];
[self debugLogRunLoopInfo:didRun];
} while (didRun && ([limitDate timeIntervalSinceNow] > 0));
}
- (IBAction)startLoop:(id)sender
{
BOOL didRun = NO;
do
{
[self myRunLoopUntilDate:runLimitDate runMode:runLoopMode];
if (self.stop)
{
break;
}
} while (YES);
}

XCode View Controller Not Updating in Do Loop

I have a do loop that I want to execute a command every 1 second while a SWITCH is on.
The Code works fine ONCE, when I don't have the DO LOOP.
However, as soon as I add the LOOP, none of the labels in the view controller are updated, the back button for the storyboard doesn't work, and the SWITCH will not toggle off. Essentially, the DO LOOP keeps looping, but nothing on the screen will work, nor can I back out.
I know I'm doing it wrong. But, I don't now what. Any thoughts would be appreciated.
I attached the code that gets me in trouble.
Thanks,
- (IBAction)roaming:(id)sender {
UISwitch *roamingswitch = (UISwitch *)sender;
BOOL isOn = roamingswitch.isOn;
if (isOn) {
last=[NSDate date];
while (isOn)
{
current = [NSDate date];
interval = [current timeIntervalSinceDate:last];
if (interval>10) {
TheCommand.text=#"ON";
[self Combo:sendcommand];
last=current;
}
}
}
else
{
TheCommand.text=#"OFF";
}
}
iOS and OSX are event based systems and you cannot use loops like this in the main (UI) thread to do what you want to do, otherwise you don't allow the run loop to run and events stop being processed.
See: Mac App Programming Guide section "The App’s Main Event Loop Drives Interactions".
What you need to do is set-up a timer (NSTimer) which will fire every second:
.h file:
#interface MyClass : NSView // Or whatever the base class is
{
NSTimer *_timer;
}
#end
.m file:
#implementation MyClass
- (id)initWithFrame:(NSRect)frame // Or whatever the designated initializier is for your class
{
self = [super initInitWithFrame:frame];
if (self != nil)
{
_timer = [NSTimer timerWithTimeInterval:1.0
target:self
selector:#selector(timerFired:)
userInfo:nil
repeats:YES];
}
return self;
}
- (void)dealloc
{
[_timer invalidate];
// If using MRR ONLY!
[super dealloc];
}
- (void)timerFired:(NSTimer*)timer
{
if (roamingswitch.isOn)
{
TheCommand.text=#"ON";
[self Combo:sendcommand];
}
}
#end
Give your processor enough time to update your view controller and not be interrupted by other processes. I give it 0.5 second before and after the view controller update signal.
[[NSRunLoop mainRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.5]];
self.CrashingTime.text = [NSString stringWithFormat: #"Crash Time = %f ms", outputOfCrashTime];
[[NSRunLoop mainRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.5]];

NSManagedObjectContext save causes NSTextField to lose focus

This is a really strange problem I'm seeing in my app. I have an NSTextField bound to an attribute of an NSManagedObject, but whenever the object is saved the textfield loses focus. I'm continuously updating the value of the binding, so this is far from ideal.
Has anyone seen anything like this before, and (hopefully) found a solution?
I encountered the issue recently and fixed it by changing the way the NSTextField was bound to the NSManagedObject attribute. Instead of binding the value of the text field to the selection.[attribute] key path of the NSArrayController, I bound the arrayController.selection.[attribute] keyPath of the view controller that had a proper outlet pointing to the controller.
For some reason, the NSTextField doesn't loose focus when the NSManagedObjectContext is saved if bound this way.
I want to share my solution. It will work for all fields without modification.
I have optimized it for this posting and removed some error checking, logging and thread safety.
- (BOOL)saveChanges:(NSError **)outError {
BOOL result = YES;
#try {
NSError *error = nil;
if ([self hasChanges]) {
// Get field editor
NSResponder *responder = [[NSApp keyWindow] firstResponder];
NSText *editor = [[NSApp keyWindow] fieldEditor: NO forObject: nil];
id editingObject = [editor delegate];
BOOL isEditing = (responder == editor);
NSRange range;
NSInteger editedRow, editedColumn;
// End editing to commit the last changes
if (isEditing) {
// Special case for tables
if ([editingObject isKindOfClass: [NSTableView class]]) {
editedRow = [editingObject editedRow];
editedColumn = [editingObject editedColumn];
}
range = [editor selectedRange];
[[NSApp keyWindow] endEditingFor: nil];
}
// The actual save operation
if (![self save: &error]) {
if (outError != nil)
*outError = error;
result = NO;
} else {
result = YES;
}
// Now restore the field editor, if any.
if (isEditing) {
[[NSApp keyWindow] makeFirstResponder: editingObject];
if ([editingObject isKindOfClass: [NSTableView class]])
[editingObject editColumn: editedColumn row: editedRow withEvent: nil select: NO];
[editor setSelectedRange: range];
}
}
} #catch (id exception) {
result = NO;
}
return result;
}
OK, so thanks to Martin for pointing out that I should read the docs a little more closely. This is expected behaviour, and here's what I did to get around it (use your judgement as to whether this is appropriate for you):
I save my context once every 3 seconds, checking at the start if the context has any changes before I bother executing the actual save: method on my NSManagedObjectContext. I added a simple incrementing/decrementing NSUInteger (_saveDisabler) to my Core Data controller class that is modified via the following methods:
- (void)enableSaves {
if (_saveDisabler > 0) {
_saveDisabler -= 1;
}
}
- (void)disableSaves {
_saveDisabler += 1;
}
Then all I do in my custom saveContext method is do a simple check at the top:
if (([moc hasChanges] == NO) || (_saveDisabler > 0)) {
return YES;
}
This prevents the save from occurring, and means that the focus is not stolen from any of my custom textfield subclasses. For completeness, I also subclassed NSTextField and enable/disable saves in my Core Data controller from the following methods:
- (void)textDidBeginEditing:(NSNotification *)notification;
- (void)textDidEndEditing:(NSNotification *)notification;
It might be a little messy, but it works for me. I'm keen to hear of cleaner/less convoluted methods if anyone has done this successfully in another way.

Resources