NSPopover padding content on one side - objective-c

I have two NSPopovers, both of which are set up exactly the same (just linking a custom NSView from IB). Both pop up just fine, but one appears to offset the content by about 20px.
This NSPopover is (properly) not padding the content...
but this one adds about 20px from the ride side.
Here are the two views laid out in IB
As you can see, the search bar should be pinned to the right side like it is the left, but for some reason it is not. At first I thought it was a contraints issue, but after messing around with them for a while I can confirm it is not.
Any clue whats up?
EDIT: Decided to subclass the view and fill its rect, got some very strange results! The view appears to be offset. I have no clue why this is...

From here, this caught my eye (emphasis mine):
The principle circumstance in which you should not call
setTranslatesAutoresizingMaskIntoConstraints: is when you are not the
person who specifies a view’s relation to its container. For example,
an NSTableRowView instance is placed by NSTableView. It might do this
by allowing the autoresizing mask to be translated into constraints,
or it might not. This is a private implementation detail. Other views
on which you should not call
setTranslatesAutoresizingMaskIntoConstraints: include an
NSTableCellView, a subview of NSSplitView, an NSTabViewItem’s view, or
the content view of an NSPopover, NSWindow, or NSBox. For those
familiar with classic Cocoa layout: If you would not call
setAutoresizingMask: on a view in classic style, you should not call
setTranslatesAutoresizingMaskIntoConstraints: under autolayout.
Does it apply to you?

if you have an outlet to your content view, you should be able to force it into place with:
[contentView setFrame:[[contentView superview]bounds]];
or more modernly I guess...
contentView.frame = contentView.superview.bounds;

Proplem solved, but I still don't exactly know why it required solving. The NSPopovers content view (or at least mine) requires an origin point of X: 13, Y: 13. For some reason, only one of my views was getting this value.
To solve this, I subclassed NSView and overrode the setFrame method forcing its x and y to always be 13, 13
-(void)setFrame:(NSRect)frameRect {
[super setFrame:NSMakeRect(13, 13, frameRect.size.width, frameRect.size.height)];
}

Related

Layer hosting NSView within NSOutlineView

I am trying to create a custom NSView that hosts a CALayer hierarchy to perform efficient display. This NSView is then embedded within a NSTableCellView that is displayed by a View-Based NSOutlineView.
The problem is that whenever I expand or collapse an item, all rows are being moved, but the layer's content remains displayed at the position it was before changing the outline.
Scrolling the NSOutlineView seems to refresh the layers and they resync with their rows at that point.
I have debugged this behavior using Instruments and it seems that the scrolling provokes a layout operation which updates the layers with a setPosition: call that should have occured when expanding or collapsing items.
Here is some sample code for a simple layer hosting NSView subclass.
#interface TestView : NSView
#end
#implementation TestView
- (instancetype)initWithFrame:(NSRect)frameRect
{
self = [super initWithFrame:frameRect];
CAShapeLayer* layer = [CAShapeLayer layer];
layer.bounds = self.bounds;
layer.position = CGPointMake(NSMidX(self.bounds), NSMidY(self.bounds));
layer.path = [NSBezierPath bezierPathWithOvalInRect:self.bounds].CGPath;
layer.fillColor = [NSColor redColor].CGColor;
layer.delegate = self;
self.layer = layer;
self.wantsLayer = YES;
return self;
}
#end
I have tried a lot of potential solutions to this problem but I couldn't find any interesting method that gets called on the NSView instance that could be overriden to call [self.layer setNeedsDisplay] or [self.layer setNeedsLayout]. I also tried various setters on the CALayer itself such as :
layer.autoresizingMask = kCALayerWidthSizable | kCALayerHeightSizable;
layer.needsDisplayOnBoundsChange = YES;
self.layerContentsRedrawPolicy = NSViewLayerContentsRedrawOnSetNeedsDisplay;
Can anyone help me figure out how to make this layer display properly inside a NSOutlineView?
I ended up answering my question. The problem wasn't in the way my TestView was implemented. I simply missed one of the steps for enabling CoreAnimation support within the application. The relevant reference is within the Core Animation Programming Guide.
Basically, in iOS Core Animation and layer-backing is always enabled by default. On OS X, it has to be enabled this way :
Link against the QuartzCore framework
Enable layer support for one or more of your NSView objects by doing one of the following
In your nib files, use the View Effects inspector to enable layer support for your views. The inspector displays checkboxes for the selected view and its subviews. It is recommended that you enable layer support in the content view of your window whenever possible
For views you create programmatically, call the view’s setWantsLayer: method and pass a value of YES to indicate that the view should use layers.
Once I enable layer support on any of the NSOutlineView's parents, the various glitches are solved.
It is difficult to read the NSOutlineView reference documents and find the information about cell reuse that is likely giving you fits here.
You may have looked at outlineViewItemDidCollapse: but it's kind of a useless for our issue, because it doesn't have a pointer to an NSView, and that's because it's older than view-based outline views.
Perhaps the one helpful mention, buried within the NSOutlineViewDelegate protocol, down in the section on view-based NSOutlineView methods, there is a single mention within outlineView:didRemoveRowView:forRow: that:
The removed rowView may be reused by the table, so any additionally inserted views should be removed at this point.
In other words, when you call the outline view's makeViewWithIdentifier:owner:, for a cellView or rowView with a particular ID you often get a recycled view. Especially often because of collapse. Incidentally, that method is from the NSTableView superclass, and in that reference, there's also this comment:
This method may also return a reused view with the same identifier that is no longer available on screen. If a view with the specified identifier can’t be instantiated from the nib file or found in the reuse queue, this method returns nil.
So you have the option of altering the view hierarchy or niling properties in didRemoveRowView:forRow. However, buried within a third cocoa reference, that for NSView, there is within the commentary on prepareForReuse, this comment:
This method offers a way to reset a view to some initial state so that it can be reused. For example, the NSTableView class uses it to prepare views for reuse and thereby avoid the expense of creating new views as they scroll into view. If you implement a view-reuse system in your own code, you can call this method from your own code prior to reusing them.
So, TL;DR, you need to implement prepareForReuse.
The pertinent references are (mostly) the superclasses of both NSOutlineView and NSTableCellView.
And, FWIW, there was a similar question here, where the questioner seems to indicate things are even worse than I think, in that NSOutlineView is more creative behind the scenes than NSTableView.
In my own work with outline views and embedded NSTextViews, I've seen wildly terrible rendering hiccups relating to expand/collapse/scroll that I seem to have managed in just the NSOutlineViewDelegate methods. On iOS they did everyone the favor of renaming makeViewWithIdentifier to the more explicit dequeueReusableCellViewWithIdentifier.
You shouldn't have to enable layer backing for any of the ancestor views (like the outline view).
In my experience, the layer immediately assigned to a view (as opposed to sublayers) doesn't need its bounds, position, or autoresizing mask to be set. It is automatically made to track the bounds of the view. In fact, I would avoid setting those properties, just in case that breaks the automatic synchronization with the view's bounds rect.
So, the question is: how are you arranging for the view to move or resize with its superview? Are you using auto layout? If so, did you turn off its translatesAutoresizingMaskIntoConstraints? If yes to both, what constraints are you setting on the view? If no to either, how did you position the view within its superview? What frame did you set? Also, is the superview configured to autoresize its subviews (probably yes, since that's the default)? What is your view's autoresizingMask?
You could also override -setFrameOrigin: and -setFrameSize: in your custom view class and call through to super. Also, add logging to show when that's happening and what the new frame rect is. Is your view being moved as you expect when you expand or collapse rows?

Subview doesn't appear on Mavericks; works on Mountain Lion

I've found a problem in my OSX app that I think is a Mavericks bug.
I have the following hierarchy:
NSView
->NSScrollView
->NSClipView
->NSTableView
When the scroll view doesn't contain any records I'm creating an overlay view as a subview of the NSView, and positioning it above the other subviews. I do this using:
[containerView addSubview:overlay
positioned:NSWindowAbove
relativeTo:nil];
FYI, the overlay view is a custom NSView subclass with a drawRect to draw the overlay itself.
On Mountain Lion this works fine, but on Mavericks the overlay does not appear. Googling around I think this is because the overlay is not being positioned above the other sibling views.
I found these links for reference:
Display Order Messed Up
Maverick Issue When Adding Subview on NSView (Stack Overflow)
The second link suggests the following code to fix the issue, which it appears to do, but as I don't have any layer-backed views in my app this feels a bit off:
[overlay setWantsLayer:YES];
Can anyone suggest a workaround for this problem other than the one suggested?
EDIT: I've found that if I put an Xcode breakpoint in the original code (without the workaround) at the point after the subview is added the subviews get added correctly, and my overlay view is displayed. If I remove the breakpoint, the overlay view is no longer displayed. Does this behaviour indicate anything?
Thanks
Darren.
It looks like you've been bitten by overlapping views:
Note: For performance reasons, Cocoa does not enforce clipping among sibling views or guarantee correct invalidation and drawing behavior when sibling views overlap. If you want a view to be drawn in front of another view, you should make the front view a subview (or descendant) of the rear view.
from Apple's Working With A View Heirarchy.
As you note, the common recommendation to fix this is to is to set a layer. However since 10.4 you can hide a view using setHidden:, see Hiding Views in the above reference. This may solve your particular problem as your overlap appears to be total (you don't want to see parts of both views, only one of them).
HTH

Xcode's auto layout is only effective in viewDidAppear and this is very problematic

After upgrading my project to iOS 6, I realized that auto layout is only effective in viewDidAppear and most of my code expects the view's frame to be available in viewDidLoad. This limitation renders the really nice auto layout feature almost useless for me. Is there any suggestions to help me use auto layout?
For example, sometimes the developer needs to adjust information about a subview based on where auto layout chooses to place that particular subview. The subview's final location cannot be ascertained by the developer until AFTER the user has already seen it. The user should not see these information adjustments but be presented the final results all at once.
More specifically: What if I want to change an image in a view based on where auto-layout places that view? I cannot query that location and then change the image without the user seeing that happen.
As a general rule, the views frame/bounds should never be relied on in viewDidLoad.
The viewDidLoad method only gets called once the view has been created either programmatically or via a .nib/.xib file. At this point, the view has not been setup, only loaded into memory.
You should always do your view layout in either viewWillAppear or viewDidAppear as these methods are called once the view has been prepared for presentation.
As a test, if you simply NSLog(#"frame: %#", NSStringFromCGRect(self.view.frame)); in both your viewDidLoad and viewWillAppear methods, you will see that only the latter method returns the actual view size in relation to any other elements wrapped around your view (such as UINavigationBar and UITabBar).
As told by #charshep in a comment, calling view.layoutIfNeeded() in viewWillAppear can do the trick.
Quote of his original comment
I had trouble getting a table view to appear at the correct scroll position when pushing it [...] because layout wasn't occurring until after viewWillAppear. That meant the scroll calculation was occurring before the correct size was set so the result was off. What worked for me was calling layoutIfNeeded followed by the code to set the scroll position in viewWillAppear.

Unable to resize a UIView

I'm using xcode 4.5 (incase that makes any difference) along with a Storyboard and I'm trying to do something seemingly very simple without any luck. In my storyboard view I have placed a UIView component as you can see in the image here http://www.shopow.co.uk/assets/storyboard-view.png where the green section is the UIView which has a height of 100
In the viewDidLoad method I've added the following code
NSLog(#"View Height: %f", self.testView.bounds.size.height);
CGRect viewFrame = self.testView.frame;
CGSize newSize = CGSizeMake(viewFrame.size.width, 300);
viewFrame.size = newSize;
self.testView.frame = viewFrame;
NSLog(#"View Height: %f", self.testView.bounds.size.height);
From what I've read about resizing stuff I'd assume the green section to resize to be 300 high however this doesn't happen although the output in the log window shows the following
2012-10-19 13:05:28.015 TableTest[54318:c07] View Height: 460.000000
2012-10-19 13:05:28.016 TableTest[54318:c07] View Height: 300.000000
Notice the initial height being 460 rather than 100 which may mean something!
iOS simulator shows this http://www.shopow.co.uk/assets/simulator-view.png
I must be overlooking something obvious as far as I can tell this is not working as expected. Any guidance would be greatly appreciated.
I'm not sure if it will fix your problem, but you should really do your view resizing in viewWillAppear rather than viewDidLoad.
viewDidLoad literally means what it says, 'the view has loaded'. However, at this point your nib may not have set up the view as you would expect.
As a test, try logging the following in both viewDidLoad and viewWillAppear
NSLog(#"frame: %#", NSStringFromCGRect(self.view.frame));
You should see that in both methods you get different results. The viewWillAppear method is more likely to be correct as the view has been set up and is ready to be presented.
I personally don't use Interface Builder all that much as it causes more headaches than I can deal with but perhaps your auto-resizing masks or elastic constraints are set which is affecting the dimensions of your view?
Like #CaptainRedmuff said, you're calling it in the wrong method. viewDidLoad is called after the view is loaded, which means the frame has already been set. The method you're looking for is loadView, as viewWillAppear:animated and viewDidAppear:animated are being called after viewDidLoad.
OP wasn't really clear how he solved it. In case anyone else is having this problem.
If your view is showing the same layout as what you see in your storyboard despite the fact that you are programmatically resizing/changing its property in some way, this is because auto-layout is causing havoc.
Just select the view that you want to programmatically change and go to File Inspector in the Attributes pane. Deselect the checkbox next to Use Auto layout. Run again and you'll see your problem magically disappear.

Objective C: Can I set a subview to be firstResponder?

I have a situation whereby I am adding a view from another viewcontroller to an existing viewcontroller. For example:
//set up loading page
self.myLoadingPage = [[LoadingPageViewController alloc]init ];
self.myLoadingPage.view.frame = self.view.bounds;
self.myLoadingPage.view.hidden = YES;
[self.view addSubview:self.myLoadingPage.view];
Is it possible to set 'self.myLoadingPage' to be the first responder? This is the case whereby the loadingpage view size does not cover the entire size of the existing view and users can still interact with the superview (which is not the desired behaviour). I want to just enable the subview in this case.
When I had a similar problem, I made an invisible UIView that covered the entire screen, I added the large invisible UIView on top of the main view and made the loading view a subview of the invisible UIView.
The simplest solution is to override hitTest method in your loading view to return TRUE. This top view is first in the responder chain, the hitTest method gets called which NORMALLY returns TRUE if the point is within the view and will therefore be handled, returning TRUE regardless means you get the touch event and effectively block the message being resent to the next responder.
Interesting question. I found a similar post with a quote from the Apple Developer Forums on this issue:
To truly make this view the only thing
on screen that can receive touches
you'd need to either add another view
over top of everything else to catch
the rest of the touches, or subclass a
view somewhere in your hierarchy (or
your UIWindow itself) and override
hitTest:withEvent: to always return
your text view when it's visible, or
to return nil for touches not in your
text view.
This would seem to indicate there isn't a terribly straightforward solution (unless there was an API change regarding this made after October, 2010.)
Alternatively, I suppose you could go through all the other subviews in your superview and individually set their userInteractionEnabled properties to NO (but that would probably prove more cumbersome than the quoted solutions).
I would love to see other ways to allow this.