Determining which NSView instance initiated a mouseDown: - objective-c

I have a gameboard with 25 tiles of myGameTile, a subclass of NSView.
In mouseDown: I want to determine which tile I clicked on and set an ivar to a representative value.
e.g. If I click on tile 12, set clickedTile to "12" or some value that uniquely represents that particular instance.
I'm open anything from the integer value 12 all the way to some sort of introspection/reflection, though built-in features and elegance are preferable to hacks, runtime wrappers, and modification. Still, I'm aware that I may have no choice but to rely on those solutions, so please to answer with those as well. I'd like to know all my options. Thanks!

You could subclass NSView and override the tag method, as written in the documentation.

You have several possibilities:
If you handle the mouseDown in the Tile view, then you need to map self to the tile ID. There are three easy ways to do this:
Pre configure Tile.tag to be the tile ID, then use self.tag.
Search for Tile in the array of Tiles to find the index [parent.tiles indexOfObject:self]
Create a dictionary mapping Tile or tile ID [[parent.tiles objectForKey:self] intValue]
Clearly, using the tag is the easiest, as long as you are not using the tag for anything else.
Alternatively, you could implement hitTest on the parent view, and return the parent view, and then handle the mouseDown in the parent view. Then mouseDown will know where the hit is and hence which tile it is.

I think hitTest: will do what you want. Something like this:
- (void)mouseDown:(NSEvent *)theEvent {
NSPoint aPoint = [theEvent locationInWindow];
NSView* viewHit = [self hitTest:aPoint];
if( viewHit != nil && viewHit != self ) {
// viewHit is our tile.
}
}

Related

Scrolling a UIScrollView in a UIScrollView

If I have a UIScrollView in a UIScrollview, but I only want the contained scrollview to recieve events and work when the parent scroll view Y Offset is 0.
How does one implement this?
Caveat I didn't mention, using self.ptViewController.collectionView.userInteractionEnabled = YES;
Does not help, as the events don't begin passing to the scrollview until the user has released their finger due presumably to the change in FirstResponder.
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView //Or – scrollViewDidScroll:
{
if (scrollView==scrollViewA && scrollViewA.contentOffset.y == 0)
{
scrollViewB.userInteractionEnabled = YES;
}
}
As far as I know, you cannot transfer the first responders between the different scrollviews so you would not have to take your finger off and on again.
However you can do something else:
Once you reach the y=0 point in your parent scrollview, you can then use the touchesMoved event to programatically scroll your contained scrollView.
Basically, you'd do a hit test to make sure the touch is in the contained scrollView, check if the container scrollView contentOffset y position is 0 and then scroll your container scrollView.
Something like:
- (void) touchesMoved: (NSSet *)touches withEvent:(UIEvent *)event
{
if (scrollViewContainer.contentOffset.y == 0)
{
if ([self hitTest:[[touches anyObject] locationInView:containedScrollView] withEvent:nil])
{
//programatically scroll your contained scrollView
}
}
}
This code is just a sample to better understand what I mean. You should adjust it to your own needs.
So, after trying the other suggestions, I found I wasn't able to replicate the fluidity needed to seamlessly scroll as if it were one table.
So long story short, a Scrollview in a scrollview, may be a bad idea if you don't want the user to take their finger off the screen while scrolling between the two.
Thus don't bother, use a UICollectionView and define precisely how each section/header/footer/row cell should be handled.
So what I did was simply add the necessary attribute changes in the Collection Flow Layout, to ensure that all sections and cells were being handled appropriately.
Due to the nature of what I was trying to achieve the flow layout required a lot of math and code to ensure all constraints were handled.

NSCell redrawing issues

I'm creating a NSCell subclass that draws some objects directly onto the view (using drawInRect:fromRect:operation:fraction:respectFlipped:hints:) and also draws an NSButton instance simply using NSView's addSubview: selector.
While objects drawn using the first method all draw correclty, I'm having problems with drawing the NSButton correctly. The issue is that my NSButton instances will draw in the right places, but multiple times over.
I've researched this on the internet for a while and some people suggested using a cache, but I'm not sure if this is efficient. (going an array containing buttons using a for loop will definately cause slow scrolling since I display a lot of data...)
How would you do this? Am I barking up the wrong tree?
This is the relevant code:
- (void)drawInteriorWithFrame:(NSRect)cellFrame inView:(NSView *)controlView
{
NSRect _controlRect = cellFrame;
float _Y = cellFrame.origin.y;
NSRect _accessoryRect = NSMakeRect(_controlRect.size.width - 70.0f, _Y + 9.0f, 50.0f, 23.0f);
_switch = [self _choiceSwitch];
[_switch setFrame:_accessoryRect];
[controlView addSubview:_switch];
}
Long story short: Friends don't let friends addSubview, while drawing.
This a fundamental, and not particularly well-explained aspect of managing control interfaces, but is important to come to grips with.
Let your controllers dictate the "order" of subviews, and you can sleep tight knowing that that button shouldn't get overtly mucked about (which is NOT the case if it's getting jostled around inside your custom drawing routines).
It's easy to get trapped in this alley, cause, like, hey, I added an NSImageView in my initWithFrame and everything seems to be okay… But it's just sort of not how you're supposed to do it, I guess… and when you start subclassing NSControl, etc. is when you start to realize why.
Updated: Here's a really good write up on designing custom controls with an equally as great sample project attached - which embodies the kind of code organization that can help avoid this type of issue. For example.. you'll notice in the controller class how he's keeping each button seperate, unique, and independent of other views' business…
for (int butts = 0; butts < 3; butts++) {
NSRect buttFrame = NSMakeRect(0, butts * 10, 69, 10);
ExampleButt *butt = [[ExampleButt alloc]initWithFrame:buttFrame];
[mainView addSubview:butt];
}
“Drawing” NSButton by adding its instance into the view hierarchy each time you draw the cell itself is definitely a bad idea. Instead, create an NSButtonCell and configure it up to your taste. Then, in your -[NSCell drawInteriorWithFrame:inView:] use a cell ivar to draw its appearance.
If you want to have a clickable NSButton instance in each cell of the table view, try to avoid a call to addSubview: when possible. Each time you do this, the control view may invalidate its layout and re-draw everything from scratch making some kind of a recursion in your case.

Mouse Enter/Exit events on partially hidden NSViews

I have a problem that I think is solvable with some hackery, but I'm very curious if there is an easier way to get the job done without having to do all of that.
I have a stack of NSViews (layer-backed, if that somehow helps provides some better solution), as shown below:
The thing here is that this is essentially a menu, but is hover-sensitive. If the user hovers over one of the exposed parts of the lower-level views, I need to perform an action depending on what that view is. It is a dynamic system so the number of stacked menu items like this may change, making static calculations more difficult. As you can see, they are basically all a copy (shape-wise) of the first item, but then rotated a bit the further you go down the stack via simple transform rotation.
My question to the SO community is what do you all think the best approach to getting mouseEntered: and mouseExited: events for just the literally visible portions of these views?
What I have attempted to do is use an NSTrackingArea on the visibleRect portion of these views, which sounds much more handy than it really is in this situation. In reality, the visibleRect seems to be "visible" for all of them, all the time. Nothing is explicitly blocked or hidden by anything more than just a partially overlapping NSView. All that happens is I get a spammed console from all of the views screaming out at once that a mouse entered their rect.
Something I am considering is making sub-NSView's of each menu item and having each of those be responsible for the tracking area... each menu item having a "strip" view along the right and bottom sides that could report, but that's still a bit of a hack and is icky.
Does anyone have a better idea? Perhaps one from experience?
Thanks!
I know you already have a solution, but I thought I would try a different approach, that didn't require getting tons of mouseMoved events. I created 3 custom views in code, added tracking rects for them and sent all mouseEntered and mouseExited messages to the same method that does a hitTest to determine which view is top most. This is the code for the content view of the window.
#implementation MainView
#synthesize oldView;
-(void)awakeFromNib {
oldView = nil;
Card *card1 = [[Card alloc]initWithFrame:NSMakeRect(150, 150, 200, 150) color:[NSColor redColor] name:#"Red Box"];
NSTrackingArea *area1 = [[NSTrackingArea alloc]initWithRect:card1.frame options:NSTrackingMouseEnteredAndExited|NSTrackingActiveInActiveApp owner:self userInfo:nil];
[self addTrackingArea:area1];
[self addSubview:card1];
Card *card2 = [[Card alloc]initWithFrame:NSMakeRect(180, 120, 200, 150) color:[NSColor yellowColor] name:#"Yellow Box"];
NSTrackingArea *area2 = [[NSTrackingArea alloc]initWithRect:card2.frame options:NSTrackingMouseEnteredAndExited|NSTrackingActiveInActiveApp owner:self userInfo:nil];
[self addTrackingArea:area2];
[self addSubview:card2];
Card *card3 = [[Card alloc]initWithFrame:NSMakeRect(210, 90, 200, 150) color:[NSColor greenColor] name:#"Green Box"];
NSTrackingArea *area3 = [[NSTrackingArea alloc]initWithRect:card3.frame options:NSTrackingMouseEnteredAndExited|NSTrackingActiveInActiveApp owner:self userInfo:nil];
[self addTrackingArea:area3];
[self addSubview:card3];
}
-(void)mouseEntered:(NSEvent *)theEvent {
[self reportTopView:theEvent];
}
-(void)mouseExited:(NSEvent *)theEvent {
[self reportTopView:theEvent];
}
-(void)reportTopView:(NSEvent *)theEvent {
id topView = [self hitTest:[theEvent locationInWindow]];
if (![topView isEqual:oldView]) {
oldView = topView;
([topView isKindOfClass:[Card class]])? NSLog(#"%#",[(Card *)topView name]):NULL;
}
}
This is the code for what I called cards (colored rectangles):
#implementation Card
#synthesize name,fillColor;
- (id)initWithFrame:(NSRect)frame color:(NSColor *)color name:(NSString *)aName{
self = [super initWithFrame:frame];
if (self) {
self.fillColor = color;
self.name = aName;
}
return self;
}
- (void)drawRect:(NSRect)rect {
[self.fillColor drawSwatchInRect:rect];
}
I finally came to a solution on Twitter via Steven Troughton-Smith. Here's how it works:
In each menu item, I am disregarding anything related to NSTrackingArea or direct mouse position interpretation. Instead, the parent controller view is handling all of the tracking and receiving mouse movement events.
Each menu item has an overridden hitTest: method that does the point conversion and returns whether or not the point being tested is within the background image (there are shadows and stuff in there, making it more difficult than the vanilla implementation).
I then setup a sort of "hover menu item changed" callback in the controller so that I can handle hover menu changes.
This was a pretty straightforward solution. Very glad I decided to stop and ask, rather than hack something together with my previous idea.
Thanks Steven!
Overlapping tracking-areas:
All you have to do is hitTest from view you are in. if this is true:
window.view.hitTest(window.mousePos) === self/*sudo code*/
What this code does is that it returns the view under the mouse position. Now all you have to do is setup a few "if" and "else" clauses to verify that your mouse is off or on the view.
Full code example:
https://gist.github.com/eonist/537ae53b86d5fc332fd3
Full description of the concept here: (perma link)
http://stylekit.org/blog/2015/12/20/Overlapping-tracking-areas/
VS the default enter and exit behaviour:
I had to add another answer to this question as this is another approach to solve the problem. This approach now also includes path assertion (think rects with round edges or other custom paths)
The answer is long winded but it works:
http://stylekit.org/blog/2016/01/28/Hit-testing-sub-views/
it involves using the apple provided method: CGPathContainsPoint(path,transform,point)
If you follow the link to that blog post and then from there check the styleKit repo on github. You will find the code need to achieve the gif animation example given above. Im providing this as a pointer to the answer as it may take you significantly less time than trying to research this on your own. I use this technique in all my UI elements and it works flawlessly.

Detecting if UIView is intersecting other UIViews

I have a bunch of UIViews on the screen. I would like to know what's the best to way to check if a particular view (which I have reference to) is intersection ANY other views. The way I am doing it right now is, iterating though all the subviews and check one by one if there's an intersection between the frames.
This doesn't seem very efficient. Is there a better way to do this?
There's a function called CGRectIntersectsRect which receives two CGRects as arguments and returns if the two given rects do intersect. And UIView has subviews property which is an NSArray of UIView objects. So you can write a method with BOOL return value that'll iterate through this array and check if two rectangles intersect, like such:
- (BOOL)viewIntersectsWithAnotherView:(UIView*)selectedView {
NSArray *subViewsInView = [self.view subviews];// I assume self is a subclass
// of UIViewController but the view can be
//any UIView that'd act as a container
//for all other views.
for(UIView *theView in subViewsInView) {
if (![selectedView isEqual:theView])
if(CGRectIntersectsRect(selectedView.frame, theView.frame))
return YES;
}
return NO;
}
To Achieve the same thing in swift as per the accepted answer then here is the function. Readymade Code. Just copy and use it as per the steps. By the way I am using Xcode 7.2 with Swift 2.1.1.
func checkViewIsInterSecting(viewToCheck: UIView) -> Bool{
let allSubViews = self.view!.subviews //Creating an array of all the subviews present in the superview.
for viewS in allSubViews{ //Running the loop through the subviews array
if (!(viewToCheck .isEqual(viewS))){ //Checking the view is equal to view to check or not
if(CGRectIntersectsRect(viewToCheck.frame, viewS.frame)){ //Checking the view is intersecting with other or not
return true //If intersected then return true
}
}
}
return false //If not intersected then return false
}
Now call this function as per the following -
let viewInterSected = self.checkViewIsInterSecting(newTwoPersonTable) //It will give the bool value as true/false. Now use this as per your need
Thanks.
Hope this helped.
In my case the views were nested, so I did this (works with both nested and not):
extension UIView {
func overlaps(other view: UIView, in viewController: UIViewController) -> Bool {
let frame = self.convert(self.bounds, to: viewController.view)
let otherFrame = view.convert(view.bounds, to: viewController.view)
return frame.intersects(otherFrame)
}
}
Hope it helps.
First, create some array that stores the frames of all of your UIViews and their associated reference.
Then, potentially in a background thread, you can run some collision testing using what's in the array. For some simple collision testing for rectangles only, check out this SO question: Simple Collision Algorithm for Rectangles
Hope that helps!

Clickable links within a custom NSCell

I have a custom NSCell with various elements inside of it (images, various text pieces) and one of those text blocks may have various clickable links inside of it. I have my NSAttributedString correctly identifying the links and coloring them blue however I can't figure out how to get the cursor to turn into a hand and allow a user to actually click on them.
Right now I have my attributed string drawn right to the cell which obviously isn't clickable, but I'm not sure how to add it any other way since NSCell doesn't inherit from NSView. Normally I'd just add an NSTextField as a subview but I can't do it like that in this case.
Any thoughts?
The only solution I can think of is via manual hit testing and mouse tracking within your NSCell. The hardest part (which I don't have the answer to) is how to determine the rect of the link text ... hopefully someone can answer that?
Once you know the rect of the url text it's possible to implement the clicking action by implementing hitTestForEvent. I think you'd do it something like this;
// If the event is a mouse down event and the point is inside the rect trigger the url
- (NSUInteger)hitTestForEvent:(NSEvent *)event inRect:(NSRect)frame ofView:(NSView *)controlView {
NSPoint point = [controlView convertPoint:[event locationInWindow] fromView:nil];
// Check that the point is over the url region
if (NSPointInRect(point, urlFrame)) {
// If event is mousedown activate url
// Insert code here to activate url
return NSCellHitTrackableArea;
} else {
return [super hitTestForEvent:event inRect:frame ofView:controlView];
}
}
Based on talks with Ira Cooke and other folks I've decided to go for the following solution:
Draw directly to the NSCell
When the mouse enters an NSCell, I will immediately add a custom NSView subview to the NSTableView at the same position as the NSCell that was hovered over.
Their designs match pixel for pixel so there's no discernible difference
This NSView will have an NSTextView (or Field, haven't decided) that will display the attributed string with links in it allowing it to be clickable.
When you hover out of the NSCell its mirror NSView is destroyed
If all goes according to plan then I should only have 1 NSView attached to the NSTableView at a time and most of the time none at all. I'll come back and report my results once I get it working.