Multiple custom controls that use mouseMoved in one window - cocoa

At first I had one window with my custom control. To get it to accept the mouse moved events I simply put in it's awakeFromNib:
Code:
[[self window] makeFirstResponder:self];
[[self window] setAcceptsMouseMovedEvents:YES];
Now I'm doing something with four of them in the same window, and this doesn't work so pretty anymore. First off, I took them out of the control's awakeFromNib and decided I'd use my appController to manage it i.e. [window makeFirstResponder:View]
My question is, how do I manage four of these in the same view if I want each one to respond to mouse moved events? Right now, I've told the window to respond to mouseMoved events but none of the views are responding to mouseMoved.

You will also need to override -acceptsFirstResponder to return YES.
#pragma mark NSResponder Overrides
- (BOOL)acceptsFirstResponder
{
return YES;
}
-mouseMoved events are expensive so I turn off mouse moved events when my control's -mouseExited message is called and I turn it on in -mouseEntered.
- (void)mouseEntered:(NSEvent *)theEvent
{
[[self window] setAcceptsMouseMovedEvents:YES];
[[self window] makeFirstResponder:self];
}
- (void)mouseMoved:(NSEvent *)theEvent
{
...
}
- (void)mouseExited:(NSEvent *)theEvent
{
[[self window] setAcceptsMouseMovedEvents:NO];
}
I quickly tested this in my custom control application. I duplicated the control several times in the nib file and it worked as expected.
You may also need:
- (void)awakeFromNib
{
[[self window] setAcceptsMouseMovedEvents:YES];
[self addTrackingRect:[self bounds] owner:self userData:NULL assumeInside:YES];
}
I don't think the -setAcceptsMouseMovedEvents is necessary, but I'm pretty sure the tracking rect code is. You may also need to experiment with the value of the assumeInside: parameter, but that is documented.

Related

iOS8: What's going on with moving views during keyboard transitions?

After switching to iOS8, I'm getting weird behavior when I move views during a keyboard transition. Can anyone explain what's going on?
Here's a minimal example to demonstrate the problem. I have a simple view with a UITextField and a UIButton. The function nudgeUp moves the text field and the button up by 10 points. It is triggered either by the buttonPressed callback, or the keyboardWillShow callback.
When I tap the button, the code works as expected: buttonPressed calls nudgeUp and the button and text field jump up by 10 points.
When I tap the text field, keyboardWillShow calls nudgeUp, but the behaviour is very different. The button and text field immediately jump down by 10 points, and then slide back up to their original position as the keyboard shows itself.
Why is this happening? How can I regain control of animations during keyboard presentation in iOS8?
#import "ViewController.h"
#implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(keyboardWillShow:)
name:UIKeyboardWillShowNotification
object:nil];
}
- (void)keyboardWillShow:(NSNotification *)notification
{
// Called when the keyboard appears.
[self nudgeUp];
}
- (IBAction)buttonPressed:(id)sender {
[self nudgeUp];
}
- (void)nudgeUp
{
CGRect newTextFieldFrame = self.textField.frame;
newTextFieldFrame.origin.y -= 10;
self.textField.frame = newTextFieldFrame;
CGRect newButtonFrame = self.button.frame;
newButtonFrame.origin.y -= 10;
self.button.frame = newButtonFrame;
}
#end
It's AutoLayout. Something changed in iOS8 and you can't just change frame or center points anymore if you have AutoLayout enabled. You have to create an outlet(s) of your constraint (vertical space) and update it accordingly instead of changing frame position. Constraints are like any other ui control and can have an outlet. Constraint change can be animated.
Example:
[UIView animateWithDuration:[notification.userInfo[UIKeyboardAnimationDurationUserInfoKey] doubleValue] delay:0 options:[[[notification userInfo] objectForKey:UIKeyboardAnimationCurveUserInfoKey] integerValue] animations:^{
self.bottomSpaceConstraint.constant = adjustmentedValue;
[self.view layoutIfNeeded];
} completion:^(BOOL finished) {
}];
You should use UIKeyboardDidShowNotification (you're using will version) and everything will work as you expect:
- (void)viewDidLoad {
[super viewDidLoad];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(keyboardDidShow:)
name:UIKeyboardDidShowNotification
object:nil];
}
- (void)keyboardDidShow:(NSNotification *)notification
{
// Called when the keyboard finished showing up
[self nudgeUp];
}
The explanation is that with UIKeyboardWillShowNotification you are changing the frames too early. After your changes the system will relayout everything to accomodate the keyboard and your changes won't have any effect.
Also, I recommend you to switch to autolayout and forget about frames.
Try using the UIKeyboardWillShowNotification userInfo to give you the frame of the keyboard. Then move the onscreen elements based on that.

How to swipe the keyboard along InteractivePopGestureRecognizer?

I was wondering how to swipe the ViewController with a visible keyboard?
in iOS 7 I can swipe the ViewController from side to side, but the keyboard stays put.
in short, I would like to get to the following state:
Thanks!
Update:
I can't recommend the original solution. While it performed well (when it performed at all), it was an unreliable hack, and could easily break the pop gesture recognizer.
My colleague Dave Lyon came up with a great solution using iOS 7 view controller transitions and packaged it up into a pod:
https://github.com/cotap/TAPKeyboardPop
Once installed, just import the main file and you should be good to go.
Original:
I'd love to know if there's a better way of doing this, but I was able to achieve the behavior by adding the keyboard's view as a subview of the view controller's main view:
- (void)viewDidLoad
{
[super viewDidLoad];
self.textView.inputAccessoryView = [UIView new];
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
[center addObserver:self
selector:#selector(keyboardWillHide:)
name:UIKeyboardWillHideNotification
object:nil];
}
- (void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (void)keyboardWillHide:(NSNotification *)note
{
if (self.textView.isFirstResponder) {
UIView *keyboardView = self.textView.inputAccessoryView.superview;
if (keyboardView) {
dispatch_async(dispatch_get_main_queue(), ^{
[self.view addSubview:keyboardView];
});
}
}
}
I've found you can also animate the keyboard with the gesture (via addTarget:action:), but the performance is abysmal and doesn't cleanly animate if the gesture is prematurely canceled.

cursorUpdate called, but cursor not updated

I have been working on this for hours, have no idea what went wrong. I want a custom cursor for a button which is a subview of NSTextView, I add a tracking area and send the cursorUpdate message when mouse entered button.
The cursorUpdate method is indeed called every time the mouse entered the tracking area. But the cursor stays the IBeamCursor.
Any ideas?
Reference of the Apple Docs: managing cursor-update event
- (void)cursorUpdate:(NSEvent *)event {
[[NSCursor arrowCursor] set];
}
- (void)myAddTrackingArea {
[self myRemoveTrackingArea];
NSTrackingAreaOptions trackingOptions = NSTrackingCursorUpdate | NSTrackingMouseEnteredAndExited | NSTrackingActiveInKeyWindow;
_trackingArea = [[NSTrackingArea alloc] initWithRect: [self bounds] options: trackingOptions owner: self userInfo: nil];
[self addTrackingArea: _trackingArea];
}
- (void)myRemoveTrackingArea {
if (_trackingArea)
{
[self removeTrackingArea: _trackingArea];
_trackingArea = nil;
}
}
I ran into the same problem.
The issue is, that NSTextView updates its cursor every time it receives a mouseMoved: event. The event is triggered by a self updating NSTrackingArea of the NSTextView, which always tracks the visible part of the NSTextView inside the NSScrollView. So there are maybe 2 solutions I can think of.
Override updateTrackingAreas remove the tracking area that is provided by Cocoa and make sure you always create a new one instead that excludes the button. (I would not do this!)
Override mouseMoved: and make sure it doesn't call super when the cursor is over the button.
- (void)mouseMoved:(NSEvent *)theEvent {
NSPoint windowPt = [theEvent locationInWindow];
NSPoint superViewPt = [[self superview]
convertPoint: windowPt fromView: nil];
if ([self hitTest: superViewPt] == self) {
[super mouseMoved:theEvent];
}
}
I had the same issue but using a simple NSView subclass that was a child of the window's contentView and did not reside within an NScrollView.
The documentation for the cursorUpdate flag of NSTrackingArea makes it sound like you only need to handle the mouse entering the tracking area rect. However, I had to manually check the mouse location as the cursorUpdate(event:) method is called both when the mouse enters the tracking area's rect and when it leaves the tracking rect. So if the cursorUpdate(event:) implementation only sets the cursor without checking whether it lies within the tracking area rect, it is set both when it enters and leaves the rect.
The documentation for cursorUpdate(event:) states:
Override this method to set the cursor image. The default
implementation uses cursor rectangles, if cursor rectangles are
currently valid. If they are not, it calls super to send the message
up the responder chain.
If the responder implements this method, but decides not to handle a
particular event, it should invoke the superclass implementation of
this method.
override func cursorUpdate(with event: NSEvent) {
// Convert mouse location to the view coordinates
let mouseLocation = convert(event.locationInWindow, from: nil)
// Check if the mouse location lies within the rect being tracked
if trackingRect.contains(mouseLocation) {
// Set the custom cursor
NSCursor.openHand.set()
} else {
// Reset the cursor
super.cursorUpdate(with: event)
}
}
I just ran across this through a Google search, so I thought I'd post my solution.
Subclass the NSTextView/NSTextField.
Follow the steps in the docs to create an NSTrackingArea. Should look something like the following. Put this code in the subclass's init method (also add the updateTrackingAreas method):
NSTrackingArea *trackingArea = [[NSTrackingArea alloc] initWithRect:self.bounds options:(NSTrackingMouseMoved | NSTrackingActiveInKeyWindow) owner:self userInfo:nil];
[self addTrackingArea:trackingArea];
self.trackingArea = trackingArea;
Now you need to add the mouseMoved: method to the subclass:
- (void)mouseMoved:(NSEvent *)theEvent {
NSPoint point = [self convertPoint:theEvent.locationInWindow fromView:nil];
if (NSPointInRect(point, self.popUpButton.frame)) {
[[NSCursor arrowCursor] set];
} else {
[[NSCursor IBeamCursor] set];
}
}
Note: the self.popUpButton is the button that is a subview of the NSTextView/NSTextField.
That's it! Not too hard it ends up--just had to used mouseMoved: instead of cursorUpdate:. Took me a few hours to figure this out, hopefully someone can use it.

NSOutlineView not refreshing when objects added to managed object context from NSOperations

Background
Cocoa app using core data Two
processes - daemon and a main UI
Daemon constantly writing to a data store
UI process reads from same data
store
Columns in NSOutlineView in UI bound to
an NSTreeController
NSTreeControllers managedObjectContext is bound to
Application with key path of
delegate.interpretedMOC
NSTreeControllers entity is set to TrainingGroup (NSManagedObject subclass is called JGTrainingGroup)
What I want
When the UI is activated, the outline view should update with the latest data inserted by the daemon.
The Problem
Main Thread Approach
I fetch all the entities I'm interested in, then iterate over them, doing refreshObject:mergeChanges:YES. This works OK - the items get refreshed correctly. However, this is all running on the main thread, so the UI locks up for 10-20 seconds whilst it refreshes. Fine, so let's move these refreshes to NSOperations that run in the background instead.
NSOperation Multithreaded Approach
As soon as I move the refreshObject:mergeChanges: call into an NSOperation, the refresh no longer works. When I add logging messages, it's clear that the new objects are loaded in by the NSOperation subclass and refreshed. It seems that no matter what I do, the NSOutlineView won't refresh.
What I've tried
I've messed around with this for 2 days solid and tried everything I can think of.
Passing objectIDs to the NSOperation to refresh instead of an entity name.
Resetting the interpretedMOC at various points - after the data refresh and before the outline view reload.
I'd subclassed NSOutlineView. I discarded my subclass and set the view back to being an instance of NSOutlineView, just in case there was any funny goings on here.
Added a rearrangeObjects call to the NSTreeController before reloading the NSOutlineView data.
Made sure I had set the staleness interval to 0 on all managed object contexts I was using.
I've got a feeling this problem is somehow related to caching core data objects in memory. But I've totally exhausted all my ideas on how I get this to work.
I'd be eternally grateful to anyone who can shed any light as to why this might not be working.
Code
Main Thread Approach
// In App Delegate
-(void)applicationDidBecomeActive:(NSNotification *)notification {
// Delay to allow time for the daemon to save
[self performSelector:#selector(refreshTrainingEntriesAndGroups) withObject:nil afterDelay:3];
}
-(void)refreshTrainingEntriesAndGroups {
NSSet *allTrainingGroups = [[[NSApp delegate] interpretedMOC] fetchAllObjectsForEntityName:kTrainingGroup];
for(JGTrainingGroup *thisTrainingGroup in allTrainingGroups)
[interpretedMOC refreshObject:thisTrainingGroup mergeChanges:YES];
NSError *saveError = nil;
[interpretedMOC save:&saveError];
[windowController performSelectorOnMainThread:#selector(refreshTrainingView) withObject:nil waitUntilDone:YES];
}
// In window controller class
-(void)refreshTrainingView {
[trainingViewTreeController rearrangeObjects]; // Didn't really expect this to have any effect. And it didn't.
[trainingView reloadData];
}
NSOperation Multithreaded Approach
// In App Delegate (just the changed method)
-(void)refreshTrainingEntriesAndGroups {
JGRefreshEntityOperation *trainingGroupRefresh = [[JGRefreshEntityOperation alloc] initWithEntityName:kTrainingGroup];
NSOperationQueue *refreshQueue = [[NSOperationQueue alloc] init];
[refreshQueue setMaxConcurrentOperationCount:1];
[refreshQueue addOperation:trainingGroupRefresh];
while ([[refreshQueue operations] count] > 0) {
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.05]];
// At this point if we do a fetch of all training groups, it's got the new objects included. But they don't show up in the outline view.
[windowController performSelectorOnMainThread:#selector(refreshTrainingView) withObject:nil waitUntilDone:YES];
}
// JGRefreshEntityOperation.m
#implementation JGRefreshEntityOperation
#synthesize started;
#synthesize executing;
#synthesize paused;
#synthesize finished;
-(void)main {
[self startOperation];
NSSet *allEntities = [imoc fetchAllObjectsForEntityName:entityName];
for(id thisEntity in allEntities)
[imoc refreshObject:thisEntity mergeChanges:YES];
[self finishOperation];
}
-(void)startOperation {
[self willChangeValueForKey:#"isExecuting"];
[self willChangeValueForKey:#"isStarted"];
[self setStarted:YES];
[self setExecuting:YES];
[self didChangeValueForKey:#"isExecuting"];
[self didChangeValueForKey:#"isStarted"];
imoc = [[NSManagedObjectContext alloc] init];
[imoc setStalenessInterval:0];
[imoc setUndoManager:nil];
[imoc setPersistentStoreCoordinator:[[NSApp delegate] interpretedPSC]];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(mergeChanges:)
name:NSManagedObjectContextDidSaveNotification
object:imoc];
}
-(void)finishOperation {
saveError = nil;
[imoc save:&saveError];
if (saveError) {
NSLog(#"Error saving. %#", saveError);
}
imoc = nil;
[self willChangeValueForKey:#"isExecuting"];
[self willChangeValueForKey:#"isFinished"];
[self setExecuting:NO];
[self setFinished:YES];
[self didChangeValueForKey:#"isExecuting"];
[self didChangeValueForKey:#"isFinished"];
}
-(void)mergeChanges:(NSNotification *)notification {
NSManagedObjectContext *mainContext = [[NSApp delegate] interpretedMOC];
[mainContext performSelectorOnMainThread:#selector(mergeChangesFromContextDidSaveNotification:)
withObject:notification
waitUntilDone:YES];
}
-(id)initWithEntityName:(NSString *)entityName_ {
[super init];
[self setStarted:false];
[self setExecuting:false];
[self setPaused:false];
[self setFinished:false];
[NSThread setThreadPriority:0.0];
entityName = entityName_;
return self;
}
#end
// JGRefreshEntityOperation.h
#interface JGRefreshEntityOperation : NSOperation {
NSString *entityName;
NSManagedObjectContext *imoc;
NSError *saveError;
BOOL started;
BOOL executing;
BOOL paused;
BOOL finished;
}
#property(readwrite, getter=isStarted) BOOL started;
#property(readwrite, getter=isPaused) BOOL paused;
#property(readwrite, getter=isExecuting) BOOL executing;
#property(readwrite, getter=isFinished) BOOL finished;
-(void)startOperation;
-(void)finishOperation;
-(id)initWithEntityName:(NSString *)entityName_;
-(void)mergeChanges:(NSNotification *)notification;
#end
UPDATE 1
I just found this question. I can't understand how I missed it before I posted mine, but the summary is: Core Data wasn't designed to do what I'm doing. Only one process should be using a data store.
NSManagedObjectContext and NSArrayController reset/refresh problem
However, in a different area of my application I have two processes sharing a data store with one having read only access and this seemed to work fine. Plus none of the answers to my last question on this topic mentioned that this wasn't supported in Core Data.
I'm going to re-architect my app so that only one process writes to the data store at any one time. I'm still skeptical that this will solve my problem though. It looks to me more like an NSOutlineView refreshing problem - the objects are created in the context, it's just the outline view doesn't pick them up.
I ended up re-architecting my app. I'm only importing items from one process or the other at once. And it works perfectly. Hurrah!

Cocoa: NSApp beginSheet sets the application delegate?

I am trying to display a custom sheet in my application, but I think I am doing something wrong. While everything seems to be working just fine, I have a rather odd side-effect. (which took hours to figure out). It turns out that everytime I display a sheet in my application, the Application delegate gets set to the instance of the sheet, thus my Controller gets unset as the delegate causing all sorts of problems.
I've created a NIB file which I called FailureSheet.xib. I laid out my interface in IB, and then created a subclass of 'NSWindowController' called 'FailureSheet.m' which I set to the File's Owner. Here is my FailureSheet class:
#import "FailureSheet.h"
#implementation FailureSheet // extends NSWindowController
- (id)init
{
if (self = [super initWithWindowNibName:#"FailureSheet" owner:self])
{
}
return self;
}
- (void)dealloc
{
[super dealloc];
}
- (IBAction)closeSheetTryAgain:(id)sender
{
[NSApp endSheet:[self window] returnCode:1];
[[self window] orderOut:nil];
}
- (IBAction)closeSheetCancel:(id)sender
{
[NSApp endSheet:[self window] returnCode:0];
[[self window] orderOut:nil];
}
- (IBAction)closeSheetCancelAll:(id)sender
{
[NSApp endSheet:[self window] returnCode:-1];
[[self window] orderOut:nil];
}
#end
Nothing complex going on here. Now this is how I display the FailureSheet in my 'Controller' class:
sheet = [[FailureSheet alloc] init];
[NSApp beginSheet:[sheet window]
modalForWindow:window
modalDelegate:self
didEndSelector:#selector(failureSheetDidEnd:etc:etc:)
contextInfo:nil];
Now if I log what the [NSApp delegate] is before displaying my sheet, it is <Controller-0x012345> which is correct. Then, after running this code and my sheet is up, if I log it again it is <FailureSheet-0xABCDEF>.
Not sure what I'm doing wrong here - Any ideas?
This is one of those "I'm-an-idiot" answers.
Turns out I at some point I accidentally made a connection in my sheet's NIB file between the Application and the File's Owner (FailureSheet) setting it as the delegate. So, everytime it got loaded it overwrote the existing delegate connection I had in my MainMenu NIB file.

Resources