NSScrollView documentView cannot be scrolled - objective-c

Coming from an iOS background, I presumed that NSScrollView would work out of the box, and I presumed that contentSize would reflect the size of the documentView passed to it. This is not the case, if the NSScrollView is created programmatically.
First issue was: why does contentSize not update when a document view is passed in.
Second issue was: why can I not scroll the scroll view, despite the fact there was more content.

The answer to the first question appears to be: don't look at contentSize, look instead at [[scrollView contentView] documentRect].
The answer to the second is that you have to explicitly set hasVerticalScroller and/or hasHorizontalScroller. The scroll view will then dynamically create NSScroller views.
You can also use setAutohidesScrollers:YES to make those appear only when necessary.

Related

Identifying correct window frame size for filling background color

I am developing in Cocoa, and I am currently having problems with filling the background of a NSWindowController.
I understand that subclassing is the way forward if you want to customise your cocoa app. So I created a custom NSView named whiteView and added this view as a subview to my windowController's contentView; however, there are some issues with completely filling the background of the window. Can anyone explain how I can have the color cover the complete surface area of the window's frame pls. Thank you
These are the results that I have so far.
1) This is the window when I leave it as it is, notice the white color only having covered half of the window.
2)Here is the same window again when I adjust the window far to the right and bottom. The white screen seems to stretch enough so that it covers the elements.
This is how I create the custom view
- (void)drawRect:(NSRect)dirtyRect
{
[super drawRect:dirtyRect];
[[NSColor whiteColor] set];
NSRectFill([self bounds]);
}
And this how I achieve plaster the view onto my window.
WhiteView *whiteBackgroundView = [[WhiteView alloc] initWithFrame:self.window.frame];
[self.window.contentView addSubview:whiteBackgroundView positioned:NSWindowBelow relativeTo:self.window.contentView];
What do I need to do to correctly allow for my window's background to be fully covered in white?
First, the simple solution is to use -[NSWindow setBackgroundColor:] to just set the window's background color. No need for a view.
If you're still interested in how to fix the view-based approach, probably what's wrong is that you haven't set the autoresizing mask of the view to make it follow the changes in the window size. For example, you could do [whiteBackgroundView setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable].
However, you could also set the whiteBackgroundView as the window's contentView rather than as a subview of it. The window's content view is always kept at the size necessary to fill the window's content rect. All of the other views of your window would be subviews of the white background view. In my opinion, this is better than making it a sibling that just happens to be at the back. Using relative ordering among siblings views to achieve a particular rendering order is a hack.
Finally, there's no reason to invoke super's implementation in your -drawRect: if the superclass is NSView itself. NSView doesn't do any drawing in its -drawRect:. Also, your subclass takes over full responsibility for the entire drawn contents of its bounds, so you'd overdraw whatever super had drawn, anyway. (Also, you need only fill dirtyRect rather than [self bounds].)
While you're at it, since your class fills its bounds, you should override -isOpaque to return YES for optimization.
Update: regarding the frame of the view: if it's not going to be the window's content view, then you want to set its frame to be its prospective superview's bounds. So, you should have used self.window.contentView.bounds if you wanted whiteBackgroundView to fill the content view.
More generally, if you want the content rect of a window, you would do [window contentRectForFrameRect:window.frame]. But if a view is going to be a window's content view, there's no need to set its frame to anything in particular. It will be resized automatically.
Update 2:
To transfer the view hierarchy from the original content view to the new content view (when you're making the white background view the content view):
NSArray* subviews = [self.window.contentView.subviews copy];
[subviews makeObjectsPerformSelector:#selector(removeFromSuperview)];
[whiteBackgroundView setSubviews:subviews];
[subviews release];
(Written for manual retain-release. If using ARC, just drop the -release invocation.)
Regarding the frame to use, as mentioned in the first update: keep in mind that the view's frame should be expressed in the coordinate system of its superview. So, as I said, self.window.contentView.bounds would work if you're putting the new view into the content view. The window's frame and content rect are in screen coordinates. They would be completely incorrect for positioning a view.

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.

Using a patternimage for a View inside an NSScrollView

I have a quite big problem, I am really not able to solve myself.
The result should look like this:
This image was made with photoshop and is part of the interface I try to build.
In the middle you see something, that should be a list of projects, you should be able to scroll, if it the list is bigger then the view.
So I am making a scrollview like this: (for some reason I cannot do this in the interface builder and want this to work programmatically)
NSScrollView *projectsListView = [[NSScrollView alloc] initWithFrame:NSMakeRect(15, 2, 801, 588)];
[projectsListView setHasVerticalScroller:YES];
Then I create the content view and set a pattern image as backgroundcolor:
NSClipView *contentView = [[NSClipView alloc] initWithFrame:NSMakeRect(0, 0,
[projectsListView frame].size.width, [projectsListView frame].size.height+(98*2))];
[contentView setBackgroundColor:[NSColor colorWithPatternImage:[NSImage imageNamed:#"BoxLineBackground"]]];
[contentView setDrawsBackground:YES];
Then set the view as document view:
[projectsListView setDocumentView:contentView];
Should work, right?
However the content view gets clipped and looks like this while scrolling:
I tried this to fix it, but it does nothing:
[[projectsListView documentView] setCopiesOnScroll:NO];
I also tried this, but it causes the contentview not to scroll at all.
The image stays the same, but I can move the scroller normally.
[[projectsListView contentView] setCopiesOnScroll:NO];
If I try to set the contentview with setContentView: instead of using setDocumentView:
it may work, but the scroller is gone, so it is also not working correctly.
I would really like to use the patternimage method, because I cannot tell how long the list will be. It depends on the user.
An additional problem then would be to get the whole thing rounded, but that does not matter that much. I tried to use a transparent border image and to overlay the NSScrollView with it using an NSImageView, but again this causes corruption, because it clips and moves the overlaying parts of the image view together with the content of the scrollview.
Anyone having an idea, how to achieve this?
Thanks
Rather than re-inventing the wheel, this interface should be implemented with a view-based NSTableView. The table cell UI could then be created in Interface Builder and you could control the background of the cells using the various NSTableView delegate methods. NSTableView will handle redraws upon scrolling correctly.
To handle the pattern color, just make the background of your cell a custom subclass of NSTableCellView and implement your pattern drawing code.
Regardless of all this, the problem you are having is due to an NSScrollView drawing optimisation. You can turn this off by calling [[yourScrollView contentView] setCopiesOnScroll:NO] on your NSScrollView instance. You can also set this in Interface Builder, just un-check the Copies on Scroll checkbox.
I fixed the problem by setting the Background Color on the NSScrollView instead on the NSClipView.
I though the background would be static in that case and I need to set it for the content view for that reason, but it works pretty well and does scroll together with the content view.
And thanks for Rob Keniger's answer. I will probably try this out.

NSScrollView incorrect content size

I have an NSScrollView that is the parent of a custom NSView subclass. The subclass uses the NSScrollView's contentSize method in order to layout its subviews.
The issue is that upon first launch, NSScrollView reports the contentSize wrong. It reports the size as being 15px more than it should be (the width of the scroller). So it seems to me that it is returning the contentSize without taking into account the scroller width; however, as soon as I adjust the frame of the scroll view (by resizing, etc.) the content size is reported properly. It seems to be just a problem upon initial launch.
Should this be reported as a bug, and are there any good solutions to this? I could use the dirty way of performing a check during layout to see if its the first time the method has been called, and then deduct 15px from the content size, but if there's something better, that would be appreciated.
I just came across a similar problem and the way I solved it was to observe the notification NSViewFrameDidChangeNotification on the Scroll view's content view (in your case the custom NSView).
This notification gets triggered when a scrollbar is added/removed from the scroll view, at which point you can reposition your view's content.
When are you checking the content size? Is it happening as your view is being instantiated from the nib file, or after the nib has been fully unarchived?
What I would probably do is invoke your layout method from -viewDidMoveToWindow: or -awakeFromNib
Are you using the option to hide/show automatically the scrollbars?