Scrolling an NSScrollView while trying to preserve the position of a subview - objective-c

I have a subclass of NSScrollView. Within that scrollView's documentView I have a couple of NSViews. Finally, I add an NSView called aView to the documentView.
I want aView to be at the bottom of the documentView as long as there's no scrolling needed. Scrolling is only possible along the y-axis.
If the documentView is too high for the contentView - so that scrolling is needed - I want aView to be displayed at the bottom of the contentView.
This works fine with the code that you see below.
Here's my Problem:
The moment I start to scroll, I want aView to stay at the bottom of the contentView but aView just scrolls with all the other views within documentView.
In other words: I want aView's position to be fixed at the bottom of the visible rect of my scrollView if scrolling is needed and to stick to the bottom of the documentView if the contentView is high enough to show the whole documentView.
Is there a way to do that? Where do I go wrong?
Here's the code in my subclassed NSScrollView:
[documentView addSubview:aView];
aView.translatesAutoresizingMaskIntoConstraints = NO;
NSLayoutConstraint *documentAViewBottom = [NSLayoutConstraint constraintWithItem:documentView attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:aView attribute:NSLayoutAttributeBottom multiplier:1 constant:0];
documentAViewBottom.priority = NSLayoutPriorityDefaultHigh;
[documentView addConstraint:documentAViewBottom];
NSLayoutConstraint *aViewMaxYEdge = [NSLayoutConstraint constraintWithItem:self attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationGreaterThanOrEqual toItem:aView attribute:NSLayoutAttributeBottom multiplier:1 constant:5];
[self addConstraint:aViewMaxYEdge];
[documentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"|[aView]|" options:0 metrics:nil views:NSDictionaryOfVariableBindings(aView)]];

I am kinda oldschool and NSLayoutConstraints don't appeal to me, so here is a alternate proposed manual solution
from your subclass of NSScrollView when you set up the document view,
[self.contentView setPostsBoundsChangedNotification:YES];
then subscribe to the bounds changed
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(contentViewDidScroll:)
name:NSViewBoundsDidChangeNotification object:self.contentView];
then in
-(void)contentViewDidScroll
{
double widthOfAView = aView.frame.size.width;
double heightOfAView = aView.frame.size.height;
[aView setFrameOrigin:NSMakePoint(NSMinX(self.contentView.bounds) - widthOfAView, NSMaxY(self.contentView.bounds) - heightOfAView)];
}
All of this assuming your isFlipped is overridden to YES, of course.

Related

programmatic NSLayoutConstraint in Mac app resizes window contentView

I'm trying to get auto layout working programmatically in an Objective-C Mac app.
The goal pretty basic, a simple toolbar-like view across the top of the window. Height should not change, view should stay at top of window, and width should resize with the window.
When I configure this in the .xib it works great. Without worrying about height, I just add leading, trailing, and top constraint connections from the custom view to the superview (which is the NSWindow's contentView). I don't have to change any priorities or other settings.
I need to do this programmatically, creating window, subview, and constraints in code. I've tried using both anchors and NSLayoutConstraint objects (both parameterized and based on visual layout string). In all cases I get a bizarre behavior where the window's contentView resizes, down to height zero, and nothing is drawn to the screen.
Here's one complete approach, using anchors:
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
// make window
NSRect windowRect = NSMakeRect(0.0, 0.0, 900.0, 200.0);
NSWindowStyleMask windowStyle = NSWindowStyleMaskTitled
| NSWindowStyleMaskClosable
| NSWindowStyleMaskResizable
| NSWindowStyleMaskMiniaturizable;
NSWindow * win = [[NSWindow alloc] initWithContentRect:windowRect
styleMask:windowStyle
backing:NSBackingStoreBuffered
defer:NO];
win.title = #"Window Made in Code";
win.contentView.translatesAutoresizingMaskIntoConstraints = NO;
// add view
NSRect viewFrame = windowRect;
viewFrame.size.height = 80.0;
viewFrame.origin.y = 120.0;
NSView * view = [[MyView alloc] initWithFrame:viewFrame];
[win.contentView addSubview:view];
[view setNeedsDisplay:YES];
// add constraints
[view.leadingAnchor constraintEqualToAnchor:win.contentView.leadingAnchor].active = YES;
[view.trailingAnchor constraintEqualToAnchor:win.contentView.trailingAnchor].active = YES;
[view.topAnchor constraintEqualToAnchor:win.contentView.topAnchor].active = YES;
// note, the above also tried using layoutMargins of superview, same behavior
// show window
[win setIsVisible:YES];
[win center];
self.codeWindow = win;
[self.codeWindow makeKeyAndOrderFront:NSApp];
}
If I don't add the constraints, everything draws correctly, but as expected the subview does not resize with the window.
When I add the constraints, the superview (window's contentView) gets resized to height 0, so nothing gets drawn. If I omit the top constraint this does not happen, however the horizontal constraints then work backward: rather than resizing subview as the window is resized, they prevent the Window contentView from resizing horizontally; I can still drag the window larger but debugging see that the content view frame width does not increase.
Here's an alternate code approach I also tried using NSLayoutConstraint factory methods; here I'm setting priority but no different priority corrects the behavior:
NSLayoutConstraint * leadingC = [NSLayoutConstraint constraintWithItem:view
attribute:NSLayoutAttributeLeading
relatedBy:NSLayoutRelationEqual
toItem:view.superview
attribute:NSLayoutAttributeLeading
multiplier:1.0
constant:0.0];
leadingC.priority = NSLayoutPriorityDragThatCannotResizeWindow;
NSLayoutConstraint * trailingC = [NSLayoutConstraint constraintWithItem:view
attribute:NSLayoutAttributeLeading
relatedBy:NSLayoutRelationEqual
toItem:view.superview
attribute:NSLayoutAttributeLeading
multiplier:1.0
constant:0.0];
trailingC.priority = NSLayoutPriorityDragThatCannotResizeWindow;
NSLayoutConstraint * topC = [NSLayoutConstraint constraintWithItem:view
attribute:NSLayoutAttributeTop
relatedBy:NSLayoutRelationEqual
toItem:view.superview
attribute:NSLayoutAttributeTop
multiplier:1.0
constant:0.0];
[NSLayoutConstraint activateConstraints:#[leadingC, trailingC, topC]];
[view.superview addConstraints:self.constraints];
Since I'm getting the same bad result trying this several different ways I feel like I must be completely missing something with NSLayoutConstraint, but I haven't been able to figure it out from tutorials or documentation. I did compare via debugging layout constraints at runtime between .xib and code-created windows but haven't been able to reconstruct what I'm doing wrong from the differences, except that the .xib superview has NSAutoresizingMaskLayoutConstraint objects.
If I comment-out win.contentView.translatesAutoresizingMaskIntoConstraints = NO; I do get resizing behavior programmatically, but that is confusing to me - I thought when we created explicit constraints we were supposed to set that flag to NO?
Thanks for any suggestions on how to get this working!
When you opt into constraints, you have to completely describe the size of the view with the constraints. In this case, you've specified the width of your toolbar, and you set it's position, but it looks like you are trying to fix the height with the frame you initially give the view. You also have to have a constraint to set its height.
Also translatesAutoresizingMaskIntoConstraints describes how a view relates to its superview, not its child views. So you should be setting that on the toolbar view, not on the contentView of the window. Try this code:
#import "AppDelegate.h"
#interface AppDelegate ()
#end
#interface MyView : NSView
#end
#implementation MyView
- (void)drawRect:(NSRect)dirtyRect
{
CGContextRef cgContext = [[NSGraphicsContext currentContext] CGContext];
CGContextSetFillColorWithColor(cgContext, [[NSColor yellowColor] CGColor]);
CGContextFillRect(cgContext, self.bounds);
}
#end
#implementation AppDelegate
{
NSWindowController *disjointController;
}
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
NSRect windowRect = NSMakeRect(0.0, 0.0, 900.0, 200.0);
NSWindowStyleMask windowStyle = NSWindowStyleMaskTitled
| NSWindowStyleMaskClosable
| NSWindowStyleMaskResizable
| NSWindowStyleMaskMiniaturizable;
NSWindow * win = [[NSWindow alloc] initWithContentRect:windowRect
styleMask:windowStyle
backing:NSBackingStoreBuffered
defer:NO];
win.title = #"Window Made in Code";
// add view
NSView *view = [[MyView alloc] initWithFrame: windowRect];
view.translatesAutoresizingMaskIntoConstraints = NO;
view.layer.backgroundColor = [[NSColor yellowColor] CGColor];
[win.contentView addSubview: view];
// add constraints
[view.leadingAnchor constraintEqualToAnchor: win.contentView.leadingAnchor].active = YES;
[view.trailingAnchor constraintEqualToAnchor: win.contentView.trailingAnchor].active = YES;
[view.topAnchor constraintEqualToAnchor: win.contentView.topAnchor].active = YES;
[view.heightAnchor constraintEqualToConstant: 80].active = YES;
// show window
[win center];
disjointController = [[NSWindowController alloc] initWithWindow: win];
[win makeKeyAndOrderFront:NSApp];
}
- (void)applicationWillTerminate:(NSNotification *)aNotification {
// Insert code here to tear down your application
}
- (BOOL)applicationSupportsSecureRestorableState:(NSApplication *)app {
return YES;
}
#end

Adding Autolayout programatically to existing autolayout Views added in xibs

I have a view like below which has auto layout set in xibs.
Now I need to add a error notification, which will animate from top and the existing views will shift down, like this image below.
I know I can do this easily by simply adding this error view in my existing view controller and managing its height constraints. But I have a couple of other views where I need to re-use this error view. So, now I have created a custom view for this error view. Now my main problem is adding this to my mainview programatically with autolayout. So I need to add the blue error view in my self.view and remove the top layout constraint of the green view and set its top to the blue error view. Make sense? Below is my code for error view and adding it to self.view. But even that doesn't work, am I doing anything wrong here. Any help is appreciated.
-(id)initWithdelegate:(id)parentSelf ForView:(UIView *)parentView
{
if (self = [super initWithFrame:CGRectMake(0, 0, 100, 100)])
{
// Initialization code
self=(ErrorView*)[[[NSBundle mainBundle] loadNibNamed:#"ErrorView" owner:nil options:nil]objectAtIndex:0];
self.delegate=parentSelf;
[parentView addSubview:self];
self.hidden = YES;
[self setConstraintsToParentView:parentView];
}
return self;
}
-(void)setConstraintsToParentView:(UIView *)parentView
{
[parentView setTranslatesAutoresizingMaskIntoConstraints:NO];
//Setting width equal to parentview
[parentView addConstraint:[NSLayoutConstraint constraintWithItem:self
attribute:NSLayoutAttributeWidth
relatedBy:NSLayoutRelationEqual
toItem:parentView
attribute:NSLayoutAttributeWidth
multiplier:1
constant:0]];
//Setting fixed height of 50
[parentView addConstraint:[NSLayoutConstraint constraintWithItem:self
attribute:NSLayoutAttributeHeight
relatedBy:NSLayoutRelationGreaterThanOrEqual
toItem:nil
attribute:NSLayoutAttributeNotAnAttribute
multiplier:1.0
constant:50]];
//Setting x pos to center
[parentView addConstraint:[NSLayoutConstraint constraintWithItem:self
attribute:NSLayoutAttributeCenterX
relatedBy:NSLayoutRelationEqual
toItem:parentView
attribute:NSLayoutAttributeCenterX
multiplier:1.0
constant:0.0]];
//Setting top position to self.view with constant 20
[parentView addConstraint:[NSLayoutConstraint constraintWithItem:self
attribute:NSLayoutAttributeTop
relatedBy:NSLayoutRelationEqual
toItem:parentView
attribute:NSLayoutAttributeTop
multiplier:1.0
constant:20]];
}
I'm calling the errorView like this from my viewcontroller
myErrorView = [[ErrorView alloc]initWithdelegate:self ForView:self.view];
you can use identifier propertiy of constraints
NSArray *ArrayConstraint = yorlableorView.constraints;
for (NSLayoutConstraint *ob in ArrayConstraint)
{
if ([ob.identifier isEqualToString:#"yourIdentifier"])
{
ob.constant = 0 ;
// set your constant you want
}
}
Could you just add the error view as the top view with a height of 0 and then change the height constraint constant when you want to display it? You can animate this and it will simulate it pushing everything down, however with this approach it won't look like it is coming down from the top of the screen.
If you're happy with this approach you could just add a container view to your view with the buttons and tableview, then after you initialise the error view from the xib just assign it to the container, this will save you having to pass the parent view, adding it as a subview and adding the constraints programmatically in your error view class.
Let me know if need more detail or a concrete example. Hope this helps, good luck. Also just as an aside, you could look into Masonry, it makes adding constraints programmatically much easier.

What can prevent Auto Layout constraints from being respected during animations?

I'm trying to snap a view to a UITabBarController's tabBar (UITabBar) to never hide it (the way Apple does it in the 'Featured' tab of the tvOS App Store).
I can make it work by setting up my constraints in a UIViewController that's directly contained in a UITabBarController. When the tab bar is hidden and shown, the view (in my case, a UICollectionView) follows perfectly with the animation. But it doesn't work as well when my UIViewController is in a UINavigationController. It eventually updates, but while the UITabBar is animating (hiding and showing), it doesn't update.
Here's how I set my constraints using NSLayoutConstraints:
UIView *targetView = self.collectionView;
NSLayoutConstraint *constraint1 = [NSLayoutConstraint constraintWithItem:targetView attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:self.tabBarController.tabBar attribute:NSLayoutAttributeBottom multiplier:1 constant:0];
NSLayoutConstraint *constraint2 = [NSLayoutConstraint constraintWithItem:targetView attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeLeft multiplier:1 constant:0];
NSLayoutConstraint *constraint3 = [NSLayoutConstraint constraintWithItem:targetView attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeHeight multiplier:1 constant:0];
NSLayoutConstraint *constraint4 = [NSLayoutConstraint constraintWithItem:targetView attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeWidth multiplier:1 constant:0];
[self.view.window addConstraint:constraint1];
[self.view addConstraints:#[constraint2, constraint3, constraint4]];
Here's how I set those same constraints with Masonry (both have the same result, but this is much more readable):
[self.containerView mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.width.height.equalTo(self.view);
make.top.equalTo(self.tabBarController.tabBar.mas_bottom);
}];
Here's a sample project demonstrating my issue:
https://www.dropbox.com/s/bxbi0gyidxhu2bz/SampleMovingWithTabBar.zip?dl=1
Is this a bug or is it expected behavior ?
I'm trying to implement this "view not hiding under the tab bar" behavior in another more complex app and I get the same odd behavior. Though in this case there are no UINavigationController involved. The view is directly in a UIViewController (which is part of the viewControllers array of the UITabBarController).
What can prevent Auto Layout constraints from being respected during animations ?
This how what my View Controller hierarchy looks like in the demo project I linked to (the View Controller that is within the UINavigationController is the one that does not animate when showing/hiding the tab bar):
<UITabBarController 0x7fca42d10bb0>, state: appeared, view: <UILayoutContainerView 0x7fca42f848b0>
| <FirstViewController 0x7fca42d11340>, state: disappeared, view: <UIView 0x7fca42f96510>
| <UINavigationController 0x7fca43812000>, state: appeared, view: <UILayoutContainerView 0x7fca42d46200>
| | <FirstViewController 0x7fca42f347f0>, state: appeared, view: <UIView 0x7fca42f8bd90>
Whenever you use CoreAnimation API's for animations at that time layout constraints do not respect constraints during transient Animations.
When you use NSLayoutconstraint's property like constant for animation then auto layout does respect constraints during transient animations.
CoreAnimations API's include UIView's block based animations. If you interested in further reading then here is the wwdc video
However, looking at your code, I don't think that's your problem. You want to respect layout constraints while UITabbar is animating. You just need to set your constraints in viewDidLayoutSubviews instead of videDidAppear and you should be all set with the existing code.
EDIT ---
I got some additional details about the problem. So first of all it's not an bug by apple. UITabBar is only responsible of it's direct children view controller. It's doesn't have any information about it's sub structure. You are responsible for chaining that responsibility.
The way to fix this is to listen to transitions calls of your UIViewController and animate accordingly.
This is the magic code. Add this to your view controller.
-(void)didUpdateFocusInContext:(UIFocusUpdateContext *)context withAnimationCoordinator:(UIFocusAnimationCoordinator *)coordinator{
NSString *prevFocusViewClassName = NSStringFromClass([context.previouslyFocusedView class]);
NSString *nextFocusedView = NSStringFromClass([context.nextFocusedView class]);
// The tabbar is going to disappear
if ([prevFocusViewClassName isEqualToString:kUITabBarButtonClassName] &&
![nextFocusedView isEqualToString:kUITabBarButtonClassName]) {
[self.view layoutIfNeeded];
[coordinator addCoordinatedAnimations:^{
[self.view layoutIfNeeded];
} completion:nil];
// The tabbar is going to appear
} else if (![prevFocusViewClassName isEqualToString:kUITabBarButtonClassName] &&
[nextFocusedView isEqualToString:kUITabBarButtonClassName]) {
[self.view layoutIfNeeded];
[coordinator addCoordinatedAnimations:^{
[self.view layoutIfNeeded];
} completion:nil];
}
}
I have fixed your code with expected output https://www.dropbox.com/s/c8jdw367a3bnb42/SampleMovingWithTabBar-2.zip?dl=0

Set vertical distance between two uibuttons using autolayout constraints

I would like ButtonA to stay where it is, and ButtonB to move a distance x vertically downward away from it.
// Using Autolayout.
UIButton *buttonA, *buttonB;
UIView *fatherView; // The superview of buttonA and B.( The view where they are inside).
CGFloat distance;
NSLayoutConstraint *constraint = [NSLayoutConstraint constraintWithItem:buttonB attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:buttonA attribute:NSLayoutAttributeBottom multiplier:1 constant:distance];
[fatherView addConstraint:constraint];
// Without autolayout, maybe more easy.
buttonB.center = CGPointMake(buttonA.center.x, buttonA.center.y + distance);

Interface Builder "locking" object to screen edges

I've read some tutorials on using AutoLayout but I just can't seem to figure out how to achieve something that I feel should be incredibly simple. I'm designing an application for both 3.5 and 4 inch iPhone/iPod Touch screens. It's a simple tab bar application with a UITableView filling up the entirety of each tab, like so:
I would like for the UITableView to lock to the edges of the screen, regardless of whether the screen is 3.5 or 4 inches. What I currently have works fine on 4-inch screens, but on 3.5 the UITableView extends beyond the width of the screen.
I've tried reading some AutoLayout tutorials as well as fiddling with Interface Builder constraints, but without success. I would appreciate any help you can provide.
You need to attach the UITableView to all edges of the parent UIView, and then the UITableView will expand or shrink to fill the UIView on the device. This will make it appear to be the proper size on all iDevices, including iPad. As pictured in the screenshot below, you can just tap all the dashed red guides, making sure the margins are set to 0 (touch the sides):
You could also Ctrl + drag left, drag right, drag up, and drag down on the UITableView choosing "Leading Space to Container", "Trailing Space to Container," "Top Space to Top Layout Guide", and "Bottom Space to Bottom Layout" respectively.
Alternatively, you could use the Visual Format Language (VFL) (code for a UIViewController below assumes your UITableView is an auto-#synthesized #property named tableView):
/* Turn off springs/structs */
self.tableView.translatesAutoresizingMaskIntoConstraints = NO;
/* Leading and trailing constraints */
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|[_tableView]" options:0 metrics:nil views:NSDictionaryOfVariableBindings(_tableView)]];
/* Top and bottom constraints */
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|[_tableView]" options:0 metrics:nil views:NSDictionaryOfVariableBindings(_tableView)]];
...or if you really like to be explicit (and like typing):
/* Leading constaint (could use NSLayoutAttributeLeft here as well) */
[self.view addConstraint:[NSLayoutConstraint constraintWithItem:self.tableView attribute:NSLayoutAttributeLeading relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeLeading multiplier:1 constant:0]];
/* Trailing constraint (could use NSLayoutAttributeRight here as well) */
[self.view addConstraint:[NSLayoutConstraint constraintWithItem:self.tableView attribute:NSLayoutAttributeTrailing relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeTrailing multiplier:1 constant:0]];
/* Top constraint */
[self.view addConstraint:[NSLayoutConstraint constraintWithItem:self.tableView attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeTop multiplier:1 constant:0]];
/* Bottom constraint */
[self.view addConstraint:[NSLayoutConstraint constraintWithItem:self.tableView attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeBottom multiplier:1 constant:0]];
The important thing with all of this is that the UITableView is a child of the view of the UIViewController (it most likely is). view will fill the screen as expected by default, and with all of the methods above, you're asking the layout to hold tight against the edges of that maximized view.
NSLayoutConstraints is ridiculous overkill for this IMHO.
Just turn it off and do this
self.tableview.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;