I'm working at a custom control that presents different handles. I want to be sure that the selected handle has a Z-index grater then the others handles.
Is there a way to swap view order?
I found this function sortSubviewsUsingFunction:context: but i can't understand whether this is the right solution.
Cocoa introduced a new API in macOS 10.0.
It's similar to iOS, you can pass another view to be displayed below or above.
[self.view addSubview:myView positioned:NSWindowBelow relativeTo:myViewInBackground];
Checkout the documentation; in my experience NSWindowBelow and NSWindowAbove seemed reversed though.
It is pretty simple, you can use a function that compare 2 subviews to reorder them. Here a simple solution based on view's tag:
[mainView sortSubviewsUsingFunction:(NSComparisonResult (*)(id, id, void*))compareViews context:nil];
...
NSComparisonResult compareViews(id firstView, id secondView, void *context) {
int firstTag = [firstView tag];
int secondTag = [secondView tag];
if (firstTag == secondTag) {
return NSOrderedSame;
} else {
if (firstTag < secondTag) {
return NSOrderedAscending;
} else {
return NSOrderedDescending;
}
}
}
Yes, sortSubviewsUsingFunction:context: can do what you're looking to do, however it might be overkill if all you'd like is for one particular view to sit on top of the (unordered) others. In that case, you should look into the bringSubviewToFront: method of UIView. That way, you could do something like this in your IBAction for bringing up the handle:
[self.view bringSubviewToFront: handle];
Assuming that handle is an object that is/inherits from UIView. If this isn't what you're looking for, then by all means, go with sortSubviewsUsingFunction:context:, in which case you'll need to alter the individual tag properties for each subview, and sort accordingly.
As i understand from you question you can loop all you view and add them to array then you have the option to add them to you view with index .. and you have the following functions.
[self.view insertSubview:view1 atIndex:2]
[self.view insertSubview:view1 aboveSubview:0]
[self.view insertSubview:view1 belowSubview:1]
then you can swap it's order as you need..
Related
I have an NSTableView that I want to have a nice looking view over when it's empty. It looks like this:
I have added the TableView inside a custom view (_superViewPackages) with the same size, and then added this code to the numberOfRowsInTableView function to show/hide the overlay:
if ( count == 0 && [[[_superViewPackages subviews] firstObject] isNotEqualTo:_viewOverlayPackages] ) {
[_superViewPackages addSubview:_viewOverlayPackages positioned:NSWindowAbove relativeTo:nil];
[_viewOverlayPackages setTranslatesAutoresizingMaskIntoConstraints:NO];
NSArray *constraintsArray;
constraintsArray = [NSLayoutConstraint constraintsWithVisualFormat:#"|-1-[_viewOverlayPackages]-1-|"
options:0
metrics:nil
views:NSDictionaryOfVariableBindings(_viewOverlayPackages)];
[_superViewPackages addConstraints:constraintsArray];
constraintsArray = [NSLayoutConstraint constraintsWithVisualFormat:#"V:|-1-[_viewOverlayPackages]-1-|"
options:0
metrics:nil
views:NSDictionaryOfVariableBindings(_viewOverlayPackages)];
[_superViewPackages addConstraints:constraintsArray];
} else if ( count > 0 && [[_superViewPackages subviews] containsObject:_viewOverlayPackages] ) {
[_viewOverlayPackages removeFromSuperview];
}
This works, but when I remove the last item in the tableView, it won't update until I either click on another tab and back again, or on another application and back again and the application redraws everything.
I have tried a number of combinations of setNeedsDisplay, setNeedsLayout and setNeedsUpdateConstraints on multiple different views but none have worked. I have also moved the updating to its own method and is invoking it with performSelectorOnMainThread to get around any threading problem.
I'm fairly new to Objective-C, so this might be obvious to someone with more experience, but after reading and testing I still can't find what really needs to be called to update.
Right now I have included this code after adding the last constraint:
[self performSelectorOnMainThread:#selector(updateView:) withObject:_viewOverlayPackages waitUntilDone:NO];
In the withObject arg I have tried adding all views I can think of but none have worked.
- (void)updateView:(NSView *)view {
NSLog(#"updateView %#", view);
[view setNeedsUpdateConstraints:YES];
[view updateConstraintsForSubtreeIfNeeded];
[view setNeedsLayout:YES];
[view layoutSubtreeIfNeeded];
[view setNeedsDisplay:YES];
[view display];
}
EDIT: Adding screenshots for clarification.
When the table view contains items, it looks like this:
But, when I remove the last item, and the TableView becomes empty, I want it to look like the screenshot at the top of the post. Instead, it looks like this until I click on another tab and back, or on another application and back etc. Then it updates correctly.
So as i can tell, the view gets added correctly, and I have checked that, It's just that the application doesn't redraw correctly until do something that makes it redraw everything (is my guess).
in ios 5 i was able to disable the double tap zoom by just overriding it with a new double tap gesture. But it seems that the double tap gesture is no longer in the gesturerecognizer array that comes with the mkmapview.
NSArray *gestureRecognizers = [_mapView gestureRecognizers];
for (UIGestureRecognizer *recognizer in gestureRecognizers) {
NSLog(#"%#", recognizer);
}
returns nothing in ios 6, where in ios 5 it would return 2 recognizers, one for single tap and one for double tap.
I'd look through the gesture recognizers of MKMapView's subviews. It's probably still there somewhere.
Of course, messing around with another view's GRs is slightly dubious and will likely break the next time Apple changes something about MKMapView...
EDIT: For the benefit of anyone else reading this, please check that it's a UITapGestureRecognizer and that numberOfTapsRequired == 2 and numberOfTouchesRequired == 1.
Also, instead of disabling double-taps on the map entirely, consider adding a double-tap GR on the annotation and then do [mapDoubleTapGR requireGestureRecognizerToFail:annotationDoubleTapGR]. Again, hacky — don't blame me if it breaks on the next OS update!
This worked for me:
[_mapView.subviews[0] addGestureRecognizer:MyDoubleTapOverrider];
Do you want to let the user do anything with the view? If not, it sufficient to set userInteractionEnabled to NO. If so, what specific interactions do you need to allow? Everything but double-tapping? Why disable that one interaction?
The more we know about your use case, the better the answers we can provide.
This works for me:
//INIT the MKMapView
-(id) init{
...
[self getGesturesRecursive:mapView];
...
}
And then let the recursive function loop through the subviews and find the GR:s.
-(void)getGesturesRecursive:(UIView*)v{
NSArray *gestureRecognizers = [v gestureRecognizers];
for (UIGestureRecognizer *recognizer in gestureRecognizers) {
if ([recognizer isKindOfClass:[UITapGestureRecognizer class]]) {
[v removeGestureRecognizer:recognizer];
}
}
for (UIView *v1 in v.subviews){
[self getGesturesRecursive:v1];
}
}
This example removes all tap-GR:s. But I guess you can specify to remove whatever you'd like.
You can use a long tap gesture instead, that works.
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.
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!
I'm writing a dynamic wizard application using cocoa/objective c on osx 10.6. The application sequences through a series of views gathering user input along the way. Each view that is displayed is provided by a loadable bundle. When the app starts up, a set of bundles are loaded and as the controller sequences through them, it asks each bundle for its view to display. I use the following to animate the transition between views
[[myContentView animator] replaceSubview:[oldView retain] with:newView];
This works fine most of the time. Every once in a while, a view is displayed and some of the subviews are not displayed. It may be a static text field, a checkbox, or even the entire set of subviews. If, for example, a checkbox is not displayed, I can still click where it should be and it then gets displayed.
I thought it might have something to do with the animation so I tried it like this
[myContentView replaceSubview:[oldView retain] with:newView];
with the same result. Any ideas on what's going on here? Thanks for any assistance.
I don't think is good to use this [oldView retain]. This retain do not make sense. The function replaceSubview will retain it if it is necessary.
Because it work "most of the time" I think it is a memory problem. You try to use a released thing. Test without it and see what happens.
I got the same problem, replaceSubview didn't work.
Finally i found something wrong with my code. so here are the rules :
Both subviews should be in MyContentview's subviews array.
OldView should be the topmost subview on MyContentView or replacesubview will not take place.
here is a simple function to perform replacesubview
- (void) replaceSubView:(NSView *)parentView:(NSView *)oldView:(NSView *)newView {
//Make sure every input pointer is not nill and oldView != newView
if (parentView && oldView && newView && oldView!=newView) {
//if newview is not present in parentview's subview array
//then add it to parentview's subview
if ([[parentView subviews] indexOfObject:newView] == NSNotFound) {
[parentView addSubview:newView];
}
//Note : Sometimes you should make sure that the oldview is the top
//subview in parentview. If not then the view won't change.
//you should change oldview with the top most view on parentview
//replace sub view here
[parentView replaceSubview:oldView with:newView];
}
}