Centering view in scroll view - cocoa

I have an NSView embedded inside a NSScrollView.
When changing the magnification in my app I'm changing the view's frame by adjusting the constraints and calling layoutSubtree:.
Now I'd like to center the same area of the view after changing the embedded view's size.
The following bits of code should be doing the trick -
the scrollers actually look updated, i.e. they expand and stick to the same position. However the scroll view doesn't seem to know about the changed position.
When starting to scroll the (updated) view manually the scroll bar positions 'jump' from the position changed via code to the actual position that's reflecting the view's scroll position.
It feels like the inverse of reflectScrolledClipView: is required after setting the scrollers via code..
Pseudocode:
double hScrollPos = NSScrollView.horizontalScroller.doubleValue;
double vScrollPos = NSScrollView.verticalScroller.doubleValue;
[view updateFrameSizeAndStuff];
[view invalidateIntrinsicContentSize];
// this seems to early though:
NSScrollView.horizontalScroller.doubleValue = hScrollPos;
NSScrollView.verticalScroller.doubleValue = vScrollPos;

Related

How Can I Make a View, With a Tab View in It, Scrollable?

I got an NSTabView inside an NSView. That NSView is in an NSClipView, which in turn is in an NSScrollView. It looks like this (NSTabView in green, and NSView in red):
As you can see, the content of NSTabView gets clipped, and no scrollbars appear (since the view doesn't expand beyond the window).
How can I make NSTabView take up as much space as it needs (doesn't clip out), and expand the NSView with it? Then, NSScrollView can deal with the scrolling of the overgrown NSView.
Since my content changes dynamically, I don't want to put in some hard values for the width and height of NSTabView's superview.
This is only part of it; here's now the overall hierarchy looks:
I want the NSTabView's superview to be scrollable instead of clipping out, like this:
The setup I'll describe is for an NSTabView that will pin to the top, left, and right sides of the scroll view. Note the NSTabView could be replaced with any other NSView, the setup is the same.
Starting with you putting a scroll view into the xib/storyboard, you'll have NSScrollView -> NSClipView -> NSView (document view). Constrain the NSScrollView to the edges of the window. Drop your NSTabView onto the NSView instance. Add constraints so that your NSTabView edge constraints equal the NSView and define a height constraint either explicitly or implicitly with other content inside the tab view that defines it.
Personally I like to change the NSView instance (document view) layout to use constraints, by default it uses autoresizing masks and this makes it difficult to keep it in sync with the NSTabView. We want the document view to be pinned to the top, left, and right sides of the scroll view. The size of this view is what determines the scrollable region so we want it to be the same size as the NSTabView so the height of the tab view will determine the scrollable area.
To change this, select the document view, and under the Size Inspector we want to change the "Layout" type to "Automatic".
Lastly, add constraints to the top, left, and right and you should be good to go.
If you want the scroll view to start at the top rather than the bottom, you should subclass the document view and override isFlipped:
class FlippedView: NSView {
override var isFlipped: Bool { true }
}

Getting NSWindow resizing right in an auto layout world

I'm having problem getting vertical window resizing working with an auto layout scroll view.
What I Want
I would like to replicate, as closely as possible, my app's current window resizing behaviour. The width of the window is flexible, but the height of the window should generally track the height of the contents. Specifically:
Normally, the window automatically resizes its height to exactly
match its contents (with the exception of #2).
The user can choose to manually resize the window so it's smaller than its
contents, in which case the imbedded scroll view will scroll the
contents. Once made shorter, the window remains that height until manually
resized or the contents becomes equal or smaller than the window again.
If the contents grows to the point that the bottom of the window
bumps into the dock or screen bounds, the window's height is pinned
to that height, just as if the user has resized it.
What I've Got
I created a window with a set of representative subviews that mimic the basic needs of my app. The window's hierarchy is simple:
Window
NSView (contentView)
NSScrollView
NSClipView (NSScrollView.contentView)
NSView (NSScrollView.documentView)
A bunch of standard and custom subviews with constraints
You can download the test project here (macOS 10.12/Xcode 8): http://mbx.cm/t/4FUGY
I've organized the various subviews so they have a flexible width, but the constraints define exactly one possible height. The parent scroll view fills the content view of the window.
The auto layout stuff works great. The window automatically resizes to match the size of the contents. If the height of the contents changes, the height of the window changes to match. Awesome.
What I Can't Get to Work
I have had no luck getting NSWindow to let me manually resize its height. The resize indicator (when hovering over the edge) shows that I can alter the window's width, but not its height.
I initially thought "Oh, that's got to be a compression resistance priority in one of the scroll views." But I can find no combination of compression resistance or hugging priorities that will alter this behavior. I've tried setting the priorities of the scroll view itself, and on the documentView (which makes no sense to me, but I tried anyway). I tried the values 749, 499, 49, and 1 in every combination I could think of.
I've searched for every problem that seemed released to this, but most of the posted questions seemed to be addressing different problems.
I added a "Dump" button to log the vertical constraints. Everything listed appears to be as expected, except for a handful of NSAutoresizingMaskLayoutConstraint objects that I don't understand. However, these appear to be constraints between the document view and the clip view, so I assume those are automatically created and aren't part of the problem.
<NSLayoutConstraint:0x608000082580 PartBoxView:0x608000140840'Part B'.height == 96 (active)>
<NSLayoutConstraint:0x608000083de0 V:[PartBoxView:0x608000140840'Part B']-(0)-| (active, names: '|':NSView:0x6080001212c0 )>
<NSLayoutConstraint:0x608000083e80 V:[PartBoxView:0x608000140370'Part A']-(NSSpace(8))-[PartBoxView:0x608000140840'Part B'] (active)>
<NSLayoutConstraint:0x608000082da0 V:|-(0)-[NSScrollView:0x6080001c10e0] (active, names: '|':NSView:0x608000121400 )>
<NSLayoutConstraint:0x608000083430 V:|-(0)-[NSView:0x6080001212c0] (active, names: '|':NSClipView:0x10040e2a0 )>
<NSLayoutConstraint:0x608000081e00 V:[NSScrollView:0x6080001c10e0]-(0)-| (active, names: '|':NSView:0x608000121400 )>
<NSAutoresizingMaskLayoutConstraint:0x650000082b20 h=-&- v=-&- NSView:0x608000121400.minY == 0 (active, names: '|':NSThemeFrame:0x102504ea0'Window' )>
<NSAutoresizingMaskLayoutConstraint:0x6500000823f0 h=-&- v=-&- NSClipView:0x10040e2a0.minY == 1 (active, names: '|':NSScrollView:0x6080001c10e0 )>
<NSLayoutConstraint:0x608000083480 V:[NSView:0x6080001212c0]-(0)-| (active, names: '|':NSClipView:0x10040e2a0 )>
<NSAutoresizingMaskLayoutConstraint:0x650000082440 h=-&- v=-&- V:[NSClipView:0x10040e2a0]-(1)-| (active, names: '|':NSScrollView:0x6080001c10e0 )>
<NSLayoutConstraint:0x608000083d40 V:|-(0)-[PartBoxView:0x608000140370'Part A'] (active, names: '|':NSView:0x6080001212c0 )>
<NSLayoutConstraint:0x608000083c50 V:[NSImageView:0x608000161bc0]-(20)-| (active, names: '|':PartBoxView:0x608000140370'Part A' )>
<NSLayoutConstraint:0x608000083bb0 V:|-(20)-[NSImageView:0x608000161bc0] (active, names: '|':PartBoxView:0x608000140370'Part A' )>
<NSLayoutConstraint:0x608000083660 NSImageView:0x608000161bc0.height == 64 (active)>
I'm hoping someone with auto layout experience can tell me how to get #2 working. I'm pretty sure #3 will require some custom code, but #2 is the big hurdle.
The Background
I'm in the process of giving my app a major facelift. It's a pretty big app, so I started by creating a set of test projects to test out some of the new UI and techniques.
The first big undertaking is converting everything to auto layout. Most of it looks like it will be fairly smooth, and I'm looking forward to some of the many benefits of auto layout.
There's no completely automated way to achieve your goal #2. You want two different modes. In one mode, the window is always made large enough to accommodate the content. If it grows, the window grows; if it shrinks the window shrinks. In the other mode, the window is allowed to be smaller than required to fit the content because the user resized it that way. In that case, if the content grows, the window doesn't grow; if it shrinks, the window doesn't shrink unless the content gets small enough that it all fits and then it switches back to the first mode.
Auto layout doesn't really do modes like this, at least not automatically. You will have to detect the mode changes and programmatically modify the constraints to implement the two behavior modes.
You have apparently created constraints between the document view and the clip view to keep their tops and bottoms coincident. That essentially forces the clip view and then the scroll view to be as large as the document view. The scroll view will never scroll because it will never be too small to show all of the content.
I think you may want two constraints for the bottom spacing. One constraint will be an inequality at required (1000) priority. You want to express that the clip view should never be bigger than the document view. The document view's bottom can be greater than or equal to the clip view's bottom, but never less than.
The second bottom spacing constraint will be an equality (with 0 constant, as now) but with priority slightly less than NSLayoutPriorityWindowSizeStayPut (500). This expresses that you want the clip view and scroll view to be large enough to fit the content, unless that would force the window to grow or prevent the user from shrinking it.
The problem is that if the window is large enough to fit the content and then the content grows, that won't force the window to grow. What I've described implements the second mode.
You could try to implement the first mode by setting the second constraint's priority higher. The problem then is that the user won't be allowed to resize the window. You're back to your current situation.
What I think you'll need to do is notice when the content resizes, by observing the document view's NSViewFrameDidChangeNotification. Be sure to tell the view to post that by setting its postsFrameChangedNotifications property to true. When the frame changes, if you think you should be in the first mode, set the second constraint's priority higher, call -layoutIfNeeded on the window, and then set the priority back down. I think you may need to defer setting the priority down to the next turn of the event loop because it's not clear that you'll get the notification after the clip view has, so maybe use GCD to schedule that.
So, how do you know which mode you should be in? I'm not entirely sure. I think it will work for the window delegate (often its controller) to implement -windowDidEndLiveResize: to know when the user has finished resizing the window. I think the user resizing it will be a live resize while programmatically resizing it or auto layout resizing it won't be.
If it was the user who resized the window, you then need to know if the user grew the window so all of the content fits or if the user sized it smaller than that. For that, you could compare the height of the document view's bounds against the clip view's documentVisibleRect.
Ken,
Thanks for the thorough and thoughtful response.
You have apparently created constraints between the document view and the clip view to keep their tops and bottoms coincident.
Well, I didn't, but IB certainly did. ;)
So the first step was to edit the clip view constraints, changing the clipView.bottom-0-documentView.bottom constraint from "equals 0" to "less than or equal to 0". That permits the clip view to be (vertically) smaller than the document view, ultimately allowing the user to resize the window vertically.
I then started with your other suggestions, adding some additional constraints to pin the height to the document and either modifying its active property or changing its priority.
Ultimately, however, I went a slightly different route. The problem is that when you ask the window's contents to grow a lot, or when it's close to the bottom of the screen, its behavior is ... well, weird.
Instead, I created a "sticky" mode for the window. When set, and the document view grows, I manually calculate a new frame for the window. I do this because I can control how the window resizes when it is near the bottom, and/or top, of the screen.
Warning
I discovered the hard way that there's a hidden danger to all of these techniques. The NSViewFrameDidChangeNotification is sent whenever the frame is resized. This can happen during auto layout. If you observe this notification and immediately adjust the window size, content size, or constraints, auto layout gets very upset and throws nasty "circular" and "recursive" layout warnings (it also sometimes fails to resize properly). The solution was to simply wrap up the window size fix in a block and queue it to execute on the main thread, after all of the auto layout logic has finished.
Here's the finished, working, test project (with comments and notes): http://mbx.cm/t/Zjdml
Here's the relevant code:
#interface ViewController ()
{
BOOL windowSizeSticky; // a change in the content size should resize the window to match
}
- (void)documentSizeChangedNotification:(NSNotification*)notification;
#end
#implementation ViewController
- (void)dealloc
{
self.view.window.delegate = nil;
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (void)viewDidLoad
{
[super viewDidLoad];
// Enable the document view to post size change notifications
NSView* docView = self.scrollView.documentView;
docView.postsFrameChangedNotifications = YES;
// Subscribe to those changes
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(documentSizeChangedNotification:)
name:NSViewFrameDidChangeNotification
object:docView];
// Queue up an initial evaluation so windowSizeSticky is set correctly
dispatch_async(dispatch_get_main_queue(), ^{
[self documentSizeChangedNotification:nil];
});
}
- (void)viewWillAppear
{
// Make this controller the window's delegate
self.view.window.delegate = self;
[super viewWillAppear];
}
- (void)windowDidEndLiveResize:(NSNotification *)notification
{
// Whenever the user resizes the window, reevaluate the windowSizeSticky mode
NSView* documentView = self.scrollView.documentView;
NSClipView* clipView = (NSClipView*)(documentView.superview);
NSRect docVisible = clipView.documentVisibleRect;
NSRect docFrame = documentView.frame;
// Update the "sticky" mode depending on whether the window now displays all, or only a portion, of the contents
windowSizeSticky = (docVisible.size.height==docFrame.size.height);
}
- (void)documentSizeChangedNotification:(__unused NSNotification *)notification
{
NSView* documentView = self.scrollView.documentView;
NSWindow* window = documentView.window;
if (!window.inLiveResize) // Suppress this logic while the user is manually resizing the window
{
dispatch_async(dispatch_get_main_queue(), ^{
// Do this the next time the main loop is idle
// This notification can be sent during auto layout, and we don't want to attempt to resize
// the window in the middle of an auto layout calculation.
// The geometry of the document view has changed; check to see if the window needs resizing
NSClipView* clipView = (NSClipView*)(documentView.superview);
NSRect docVisible = clipView.documentVisibleRect;
NSRect docFrame = documentView.frame; // The doc's frame is in the clip view's coordinate system
if (docVisible.size.height==docFrame.size.height)
{
// All of the document is (vertically) visible in the clip view
// That means the window is displaying all of its contents
// Whenever this happens, switch to "sticky" mode so future changes in content will make the window grow
windowSizeSticky = YES;
}
else if (windowSizeSticky && docVisible.size.height < docFrame.size.height)
{
// The content is now taller than the view port of the scroll view & the window is "sticky"
// Try to make the window taller so all of the content is exposed
NSRect windowFrame = window.frame;
CGFloat addHeight = docFrame.size.height-docVisible.size.height;
NSRect contentRect = [window contentRectForFrameRect:windowFrame];
contentRect.size.height += addHeight;
// Calculate an ideal window frame, then adjust the existing frame so it's as close as we can get
NSRect targetFrame = [window frameRectForContentRect:contentRect];
CGFloat deltaY = targetFrame.size.height-windowFrame.size.height;
if (deltaY >= 1.0)
{
// The window needs to be taller
// Make it tall enough to display all of the content, keeping its title bar where it is
windowFrame.origin.y -= deltaY;
windowFrame.size.height += deltaY;
// Screen bounds check...
NSRect visibleFrame = window.screen.visibleFrame;
if (visibleFrame.origin.y>windowFrame.origin.y)
{
// The bottom of the window is now below the visible area of the screen
// Move the whole window up so it's back on the screen
windowFrame.origin.y = visibleFrame.origin.y;
if (visibleFrame.origin.y+visibleFrame.size.height < windowFrame.origin.y+windowFrame.size.height)
{
// The top of the window is now off the top of the screen
// Shorten the window so it's entirely within the screen
windowFrame.size.height = visibleFrame.size.height;
// This also means "sticky" mode is off, since we had to size the window to something smaller
// than its contents.
windowSizeSticky = NO;
}
}
[window setFrame:windowFrame
display:NO
animate:NO/* be consistent; constraints doesn't animate when getting shorter */];
}
}
// else { window is not sticky OR its contents doesn't exceed the height of the window: do nothing }
});
}
}
#end

UIScrollView contentInset is set to nonZero when using autolayout

So I have the following view hierarchy :
A full size scrollView in my viewController's view with the following constraints :
These are the constraints on containerView (ignore the second last one, its a hacky semi-fix for my problem):
I have the status bar, the navigation bar and the tab bar visible.
The thing is that when I set a breakpoint to check the scrollView's contentInset, it shows 64 on top and 49 on bottom, left and right are zero.
There is no way to set contentInset in IB, I tried setting it in an IBAction to UIEdgeInsetZeio, but that didn't fix it either. This is screwing up my scrollview by adding space above and below my contentView, how can I fix this?
By default the view controller extends the view under the top navigation bar so your content will blur under a translucent navigation bar. This is controlled by edgesForExtendLayout which is managed in Storyboard via the Extend Edges setting.
By default, the scrollview will automatically adjust its content inset so the content appears below the top layout guide. This is controlled by automaticallyAdjustsScrollViewInsets which is also managed in Storyboard.
What you did was constrain the top of your scroll view to the top layout guide instead of the top of its superview. By doing this, you manually offset it by 64 points. However, the scrollview is still automatically insetting its content by 64 points, which is why you're seeing additional space above and below your scroll view.
Either constrain your scrollview to its superview (so its content scrolls under the top/bottom bars), or disable the view controller from automatically adjusting the scroll view inset.

How do I alter the position of a UIImage inside a UIImageView

I have a UIImage called image I want to change the position of it inside the imageView so it can be dragged down slightly, roughly 30px. Can some one tell me how to do it? This is where I am up to but its not coming out correct.
var image = UIImage()
var imageView = UIImageView(frame: CGRectMake(0, 0, view.frame.size.height * 0.22, view.frame.size.height * 0.22))
imageView.center = CGPointMake(self.view.center.x, view.frame.size.height * 0.414)
imageView.image = self.image
imageView.layer.cornerRadius = imageView.frame.size.width / 2
imageView.layer.borderWidth = 2.0
imageView.layer.borderColor = UIColor(red: 254.0/255, green: 216.0/255, blue: 0/255, alpha: 1.0).CGColor
imageView.clipsToBounds = true
imageView.layer.contentsRect = CGRectMake(0, 20, imageView.frame.size.width, imageView.frame.size.height) //This is where I have being trying to do it but no success.
imageView.contentMode = .ScaleAspectFill
view.addSubview(imageView)
Short answer: You don't.
What you would do is add the image view as a subview of another view. The trivial way to do this would be to put the image view in a scroll view, constrained so that the only place it can scroll is down, and only slightly. Then you could achieve the scrolling with zero code.
EDIT:
This isn't really a coding problem - it's more of an Interface Builder problem. You need to set up a scroll view.
A scroll view is a view that lets you look at a portion of a larger view.
You can think of a scroll view like a piece of paper with a rectangular hole in it. You put a bigger piece of paper under it (The scroll view's content view) and you can slide the bigger piece of paper around and view different parts of it through the hole.
Here's how you would set it up.
Drag a scroll view onto your view controller. Size it and add constraints to it to position it where you want. If you want your image view to be 300x300 points in size, for example, and want to be able to drag it up or down by 20 points, then make the scroll view 20 points taller. (w: 300, h: 320)
Select the view inside the scrollview and set it's width to the same width as it's scrollview, but 20 points taller than the scroll view. (w: 300, h: 340). Add constraints to lock it's height and width.
Now you have a scroll view that's big enough for a 300x300 point image, with 20 points of total white space at the top and bottom.
You've created a content view that's 20 points bigger than that, so it can slide up or down by 20 points in the scroll view.
Drag your 300x300 point image view into the view inside the scroll view, assign an image to it, and add constraints to lock it's size and center it horizontally and vertically in it's superview.
The final step is to set the content size of the scroll view. Normally you just set a scroll view's content size to the size of it's content view. You can do that by adding this bit of code to your view controller's viewDidLoad:
(Assuming you've connected an outlet to your scrollview that's called theScrollView)
//Get the first (and only) subview of the scrollView.
let subview = theScrollView.subviews[0] as! UIView;
//Make the scroll view's contentSize the same size as the content view.
theScrollView!.contentSize = subview.bounds.size;
It's also possible to set the content size of the scroll view without any code at all. You'd use a feature of IB (Interface Builder) called "User Defined Runtime Attributes". Here's how you'd do that: (If you use this approach don't add the code above to viewDidLoad)
Select the scroll view in IB.
Press command-option 3 to select the "identity inspector".
In the section headed "User Defined Runtime Attributes", tap the plus sign on the left. Edit the Key Path to "contentSize" (all lower case except the "S" in "Size". That's very important.) Press enter to change the key path. Then tap on the "type" column and select "size". The value field will show "{0,0}". Enter your desired content size: ("{300,340}" in the example above.)
What this does is tell IB "At runtime, look for a property called "contentSize" in the selected object (the scroll view.) Set that property to the specified value of type CGSize.
Once you're done your IB "identity inspector" should look like this:
Note that if you get a key name wrong when using "User Defined Runtime Attributes" then the app crashes when you display that view controller, with a very cryptic message.
By default scrollviews let you "overshoot" when dragging their contents around, and then bounce back into place when you let go. You can turn that feature off by unchecking the "Bounce" checkbox in the IB "Attributes Inspector" (command option 4)
You shouldn't do that. But if you want to you can play with anchorPoint property of backing layer of UImageView. link
Note: Keep in mind any layout process may alter this property later.

NSView controls not resizing?

I have a NSView with a NSTableView inside of it.
If the view looks like this:
And the size & position properties for the Scollview (and tableview) look like this:
Then why when I resize the view, does it look like this:
Are my autosizing properties not set correctly? To my understanding they should be?
To make it clearer, you won’t see autoresizing behaviour whilst designing your view in Interface Builder unless the border of the subview coincides with the corresponding border of the superview and Live Autoresizing is enabled.
This behaviour exists because resizing the superview can be used to indicate that you want a certain margin between the subview area and the superview area — for instance, you might have a 100pt margin between the subview’s right border and the superview’s right border, and then increase that margin to, say, 200pt by dragging the superview handles.
Edit: On the other hand, if you want to resize the superview whilst keeping the margins according to the autoresizing mask, you can drag the superview handles whilst holding the alt/option key.
Use Cocoa Simulator (File -> Simulate Interface) in order to test your interface, including autoresizing behaviour.
The table with its scroll view will autoresize to your view's size only if you enter dimensions in by hand in Size info panel. They will not autoresize if you drag view's handles with mouse.

Resources