How to install a floating layout constraint programmatically - objective-c

According to Apple, we can create floating views in a scroll view by adding a constraint to something outside of the scroll view.
Evidence #1:
Note that you can make a subview of the scroll view appear to float
(not scroll) over the other scrolling content by creating constraints
between the view and a view outside the scroll view’s subtree, such as
the scroll view’s superview.
Evidence #2:
Creating Anchored Views Inside a Scroll View
You may find you want to create an area inside a scroll view that doesn’t move when a user scrolls the contents of the scroll view. You accomplish this by using a separate container view.
This works great when the constraint is created before the scroll view has scrolled (e.g. via Interface Builder), but if I try to create said constraint after a scroll view has been scrolled, I get incorrectly positioned views.
How do I create this anchored or floating constraint programmatically even after the scroll view has been scrolled?
I am using the following code to install the constraint:
NSLayoutConstraint *constraint = [NSLayoutConstraint constraintWithItem:self.pinkView
attribute:NSLayoutAttributeBottom
relatedBy:NSLayoutRelationEqual
toItem:self.scrollViewContainer
attribute:NSLayoutAttributeBottom
multiplier:1
constant:0];
[self.scrollViewContainer addConstraint:constraint];
And this is what I want it to look like:
(Notice how the pink view is vertically aligned with the cyan view.)
However, if the user scrolls and then taps the button, this is what it ends up like:

TLDR: Floating constraints are special in that they constantly update their constant property as the scroll view is scrolled. Thus, when you create your constraint, you must ensure that is in sync with the scroll view by pre-populating the constant to equal the scroll view's content offset value.
In the simplest example, assume we have a scroll view with a button. When the button is tapped, a floating view should appear within the scroll view.
If the user scrolls before tapping the button, the floating view will not be aligned properly.
The problem is that when you create a floating view via a constraint, you are creating a special constraint which will constantly update its constant value as the scroll view scrolls. (Other constraints never update their constant value as things move around.) When you initially create the constraint, the constant value is initialized to 0, and thus has no idea about what happened to the scroll view so far. Auto layout has not had a chance to sync it with the scroll view since it wasn't installed previously, and thus there was no way for auto layout to know about it.
The reason it works when the constraint is installed before scrolling is because it was installed at 0, and then auto layout is updating its constant automatically for you. Thus you "accidentally" installed the constraint and synced it with the scroll view (since the offset starts off at 0).
By overriding scrollViewDidScroll:, we see evidence of this auto-updating constant:
Constraint constant: 95.00
Constraint constant: 98.50
Constraint constant: 101.50
Constraint constant: 105.50
Constraint constant: 106.50
The solution is to install the constraint with the constant property set to the current scroll view offset:
NSLayoutConstraint *constraint = [NSLayoutConstraint constraintWithItem:self.pinkView
attribute:NSLayoutAttributeBottom
relatedBy:NSLayoutRelationEqual
toItem:self.scrollViewContainer
attribute:NSLayoutAttributeBottom
multiplier:1
constant:self.scrollView.contentOffset.y];
[self.scrollViewContainer addConstraint:constraint];
Other interesting things to note:
If you attempt to install the constraint with a 0 constant on view did appear and the view controller has a navigation bar, it will fail since the scroll view's position has already been moved to 64, which is not the same as 0.
Another way to think of this is this way:
Auto Layout is dumb. There is no way to set the floating view's position to anything but a relative value to the scroll view. Thus Apple was able to fake this effect by doing something tricky: go through all constraints that involve floating views, and continually update their constant property to reflect the scroll view's content offset.

Another alternative would be to make your scroll view a UICollectionView instead and make the floating view a supplementary view. Then you float it in your layout by implementing layoutAttributesForElementsInRect:
-(NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {
NSArray *layoutAttributes = [super layoutAttributesForElementsInRect:rect];
NSMutableArray *mutableAttributes = [layoutAttributes mutableCopy];
UICollectionViewLayoutAttributes *floatingViewAttributes = [UICollectionViewLayoutAttributes layoutAttributesForSupplementaryViewOfKind:#"my floating view" withIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]];
CGFloat floatingViewY = (self.collectionView.frame.size.height - floatingViewHeight) + self.collectionView.contentOffset.y;
CGRect frame = CGRectMake(floatingViewX, floatingViewY, floatingViewWidth, floatingViewHeight);
floatingViewAttributes.frame = frame;
floatingViewAttributes.zIndex = someLargePostiveInteger;
[mutableAttributes addObject:floatingViewAttributes];
return mutableAttributes;
}
A UICollectionView might not be appropriate for your application, but I thought I'd throw it out there as a possibility.

Related

Adding constraints to a ViewController secondary UIView

I have a Storyboard setup with a dialog box that utilizes a secondary UIView (yellow in the screenshot.) This is done by dragging a UIView from the objects library up in between First Responder and Exit on the Storyboard.
I can't figure out how to add basic constraints to this secondary UIView relative to the superview. Right click and dragging won't let me apply constraints. Is this possible? Do I need to handle all Autolayout / constraints programmatically?
I just need to add constraints from leading / trailing. Height will be fixed, but may adjust accordingly depending on screen size.
This has been figured out. You can not use right-click & drag constraints in IB on these views. Why?? Because they're not a part of the view hierarchy yet! (silly me..)
In my above example with the yellow secondary view, here's how you would apply auto layout to it using VFL (Visual Format Language).
In ViewDidLoad, you first want to turn off the views auto resizing mask:
self.yellowSecondaryView.translatesAutoresizingMaskIntoConstraints = false
Now find where you're adding the subview. Let's say you use a button action to trigger it... Inside that action, you first add the view (1), and then the constraints (2):
// 1
view.addSubview(yellowSecondaryView)
// Create a string representation of the view to use it with VFL.
let viewsDictionary: [String: UIView] = ["yellowSecondaryView": yellowSecondaryView]
//2
view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|-50-[yellowSecondaryView]-50-|", options: [], metrics: nil, views: viewsDictionary)) // Width of yellow view will be full width, minus 50 points from the LEFT and the RIGHT of the superview (|)
view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:[yellowSecondaryView(250)]", options: [], metrics: nil, views: viewsDictionary)) // Set a static height of 250 for the yellow view
view.addConstraint(NSLayoutConstraint(item: yellowSecondaryView, attribute: .centerY, relatedBy: NSLayoutRelation(rawValue: 0)!, toItem: view, attribute: .centerY, multiplier: 1, constant: 0)) // Center the view vertically in the superview. (credit to #rdelmar in this thread: http://stackoverflow.com/questions/21494039/using-autolayout-in-ib-how-do-i-position-a-uiview-in-the-center-of-the-screen-pr/21494707#21494707)
Hope this helps somebody create a much more manageable and organized storyboard! :)

Autolayout: how to hide UIView containing subViews?

The best solution for hiding views with the new Autolayout is definitely to create height constraint for the view, connect it and create an outlet for it, and change self.myViewHeightConstriant.constant equals to 0. But suppose the view contains some other views, suppose an imageView and some label below it. Now, the imageView is 10px away from the top and has top space to superview constraint with 10px value. Trying to hide container UIView with constant = 0 shows an error in console:
Unable to simultaneously satisfy constraints.
Probably at least one of the constraints in the following list is one you don't want.
Try this: (1) look at each constraint and try to figure out which you don't expect;
(2) find the code that added the unwanted constraint or constraints and fix it.
(Note: If you're seeing NSAutoresizingMaskLayoutConstraints that you don't
understand, refer to the documentation for the UIView property
translatesAutoresizingMaskIntoConstraints)
(
<NSLayoutConstraint:0xc7cedb0 V:[UIView:0xc7ce1e0(0)]>,
<NSLayoutConstraint:0xc7ceea0 V:[UIImageView:0xc7ce270]-(0)-| (Names: '|':UIView:0xc7ce1e0 )>,
<NSLayoutConstraint:0xc7cef30 V:|-(10)-[UIImageView:0xc7ce270] (Names: '|':UIView:0xc7ce1e0 )>
)
Will attempt to recover by breaking constraint
<NSLayoutConstraint:0xc7ceea0 V:[UIImageView:0xc7ce270]-(0)-| (Names: '|':UIView:0xc7ce1e0 )>
The guess the problem is container UIView has height 0, but the imageView has top space offset 10px from it and Autolayout engine doesn't understand how to handle this situation. Tried to set clipSubviews for container view but that didn't help. Any ideas?
UPDATE several thoughts, creating an outlet topSpaceToSuperView constraint for the imageView and set its constaint also to 0 doesn't look very appealing. There should be more elegant solution than trashing the code with multiple outlets...
You can't go simple with container.hidden = YES?
Otherwise, it's the bottom constraint that's breaking things. #"V:|-10-[imageView]|" tells the container view that has to be at least 10 pts tall. But #"V:|-10-[imageView]" would be fine.
Perhaps instead of anchoring the imageView to the bottom of the container, setup a constraint for the imageView's height.
[NSLayoutConstraint constraintsWithVisualFormat:#"V:|-10-[imageView]"
options:nil
metrics:nil
views:views];
[NSLayoutConstraint constraintWithItem:self.imageView
attribute:NSLayoutAttributeHeight
relatedBy:NSLayoutRelationEqual
toItem:self.containerView
attribute:NSLayoutAttributeHeight
multiplier:1.
constant:-10.f];
Update
You mention in the comments that the imageView isn't a predictable height. Since that's the case, it might be easier to just manage the container's height, but do it with separate constraints:
containerOpen = [NSLayoutConstraint constraintsWithVisualFormat:#"V:|-10-[imageView]|"
options:nil
metrics:nil
views:views];
containerClosed = [NSLayoutConstraint constraintsWithVisualFormat:#"V:[containerView(0)]"
options:nil
metrics:nil
views:views];
// Toggle between the constraints to open close the container
- (void)toggleContainer
{
[self.containerView.superview removeConstraints:containerOpen];
[self.containerView.superview removeConstraints:containerClosed];
self.containerView.isOpen = !self.containerView.isOpen;
if (self.containerView.isOpen)
[self.containerView.superview addConstraints:containerOpen];
else
[self.containerView.superview addConstraints:containerClosed];
[self.containerView.superview setNeedsUpdateConstraints];
[self.containerView setNeedsLayout];
[self.containerView layoutIfNeeded];
}
If you have a leading, trailing, top, or bottom constraint you can set just set the relation to LessThanOrEqual when you hide it, and then back to equal when you show it.
Because relation is read-only, you'd do this by:
Outletting the constraint
Programmatically removing it from
the superview
Setting the outlet equal to a new constraint that
has the same parameters, except with your desired <= or = (depending
on if you are hiding or showing
Re-adding the constraint to the
superview
Essentially, all you're doing here is making that constraint small enough so that the height of your "big" view can == 0 when it's hidden.

Autolayout and Constraints for iPad

I'm quite confused about the new auto layout feature of xCode 4.5.
Here is I want to do,
By setting the storyboard, I have this portrait view set up.
By using autolayout and constraints(and pins), how can I transform the layout when its flipped to landscape like this?
I tried coding and changing the CGRect(size and coordinate location) of the views when its landscaped but to no avail.
NSLayoutConstraints replace the need for CGRects in Auto Layout. First, describe your layout in words. Here's how I'd describe your portrait example:
Red's width is 60% of its superview's.
Blue's height is 55% of its superview's.
Blue's left & right edges are touching its superview's.
Red's left edge is touching its superview's, red's right edge is close to yellow's left edge, and yellow's right edge is touching its superview's.
Blue's top edge is touching its superview's, blue's bottom edge is close to red's top edge, and red's bottom edge is touching its superview's.
Blue's bottom edge is close to yellow's top edge, and yellow's bottom edge is touching its superview's.
Here's a method that removes superview's existing constraints, then applies new constraints for the given interface orientation.
- (void) buildConstriantsForInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {
// Remove existing constraints.
[superview removeConstraints:superview.constraints] ;
// Build an array to hold new constraints.
NSMutableArray* constraints = [NSMutableArray new] ;
// Add 2 constraints that apply to both Portrait & Landscape orientations.
[constraints addObject:[NSLayoutConstraint constraintWithItem:red attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:superview attribute:NSLayoutAttributeWidth multiplier:0.6 constant:0]] ;
[constraints addObject:[NSLayoutConstraint constraintWithItem:blue attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:superview attribute:NSLayoutAttributeHeight multiplier:0.55 constant:0]] ;
// Build a dictionary to store references to NSViews.
NSDictionary* views = NSDictionaryOfVariableBindings(superview, blue, red, yellow) ;
// To eliminate repeated NSLayoutConstraint code, build an array of Format Strings with which to build constraints.
NSArray* formatStrings ;
if ( UIInterfaceOrientationIsPortrait(interfaceOrientation) ) {
formatStrings = #[#"H:|[blue]|", #"H:|[red]-[yellow]|", #"V:|[blue]-[red]|", #"V:[blue]-[yellow]|"] ;
}
else {
formatStrings = #[#"H:|[blue]-[yellow]|", #"H:|[red]-[yellow]", #"V:|[blue]-[red]|", #"V:|[yellow]|"] ;
}
for ( NSString* formatString in formatStrings ) {
[constraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:formatString options:0 metrics:nil views:views]] ;
}
// Add the newly created constraints.
[superview addConstraints:constraints] ;
}
You can call that method whenever the view loads or rotates.
-(void) viewDidLoad {
superview.translatesAutoresizingMaskIntoConstraints = NO ;
[self buildConstriantsForInterfaceOrientation:self.interfaceOrientation] ;
}
- (void) willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration {
[self buildConstriantsForInterfaceOrientation:toInterfaceOrientation] ;
}
Autolayout is great at expressing the relation of one object to another and resolving conflicts - but it doesn't have any conditional concepts built in. For your layout I think you may find it easiest to add and remove constraints on rotation, check out https://developer.apple.com/library/ios/#documentation/AppKit/Reference/NSLayoutConstraint_Class/NSLayoutConstraint/NSLayoutConstraint.html for the details on how to add those.
You can also set up your constraints and adjust the priorities so that it does the right thing on rotation but it will take some testing and adjustment to get it right on. I did some testing locally and I think I have it doing the right thing but that's just with empty views that have no inherent content size. Nevertheless, here is what I think is required to do it all via storyboard:
Make sure the blue view has a width <= the portrait width and >= the minimum landscape width
Make sure the red view has a minimum height
Fix the yellow view's width, and set the height to be >= the portrait height
Anchor each view to the corner it will always remain in (eg, pin trailing and bottom to superview for yellow)
Align the top of the yellow view with the blue view, with a low priority
Increase content compression resistance for yellow
I think of it as starting with the blue view having a fairly high priority maximum size, and the yellow view knowing how to expand upwards but being low priority. When it rotates, the blue view keeps its maximum size which frees the yellow view to expand upwards. Then I fill in the needed constraints to keep things aligned.
This is fairly complicated to describe in text, here is a screengrab showing the three views and the constraints between them. It doesn't show everything, but you can at least inspect the relations between the views:

Subview disappears with auto layout

I just added a UIView as subview to the main UIView on interface builder (basic single view application).
Without setting any constraints, my subview disappears.
subview's frame = (0 0; 320 0);
Why is that?
If I try to add some constraints like for trailing space, leading space, top space and bottom space to be fixed, still my view disappears.
How can I solve this?
Thank you.
Just to clarify thing a little I created a test project (single view application), and added 2 subview to the main view like in the image. I didn't change any default constraint.
And you can see the error in the log of the image.
Logs:
**2013-01-19 17:16:02.435 Test[8871:c07] Unable to simultaneously satisfy constraints.
Probably at least one of the constraints in the following list is one you don't want. Try this: (1) look at each constraint and try to figure out which you don't expect; (2) find the code that added the unwanted constraint or constraints and fix it. (Note: If you're seeing NSAutoresizingMaskLayoutConstraints that you don't understand, refer to the documentation for the UIView property translatesAutoresizingMaskIntoConstraints)
(
"<NSAutoresizingMaskLayoutConstraint:0x106178c0 h=--- v=--- V:[UIWindow:0x9917040(480)]>",
"<NSLayoutConstraint:0x106159e0 UIView:0x991a5a0.bottom == UIWindow:0x9917040.bottom>",
"<NSLayoutConstraint:0x991ab00 V:|-(518)-[BottomView:0x9919c90] (Names: '|':UIView:0x991a5a0 )>",
"<NSLayoutConstraint:0x10615960 V:|-(20)-[UIView:0x991a5a0] (Names: '|':UIWindow:0x9917040 )>",
"<NSLayoutConstraint:0x991aa80 BottomView:0x9919c90.bottom == UIView:0x991a5a0.bottom>"
)
Will attempt to recover by breaking constraint
<NSLayoutConstraint:0x991aa80 BottomView:0x9919c90.bottom == UIView:0x991a5a0.bottom>
Break on objc_exception_throw to catch this in the debugger.
The methods in the UIConstraintBasedLayoutDebugging category on UIView listed in <UIKit/UIView.h> may also be helpful.**
Constraints:
Also here is the result on the simulator:
It is good practice to understand those logs but if you are going to use Autolayout you are going to have to read up on this. Everyone says it is simply but I personally have not found it simple.
Apples Programming Guide For AutoLayout
Please read this guide especially the debugging section.
As a very very general rule if you are going to add a view then you need to turn off autoresizingmasks (Spring and struts) for the view. Add the view as a subview and give it 2 or 3 constraints. In your case above you would give it a constaint that it should have a left or leading space to superview of 0. A top space to superview of 0 and a width of 320.
EDIT; Here is an example of adding a view; note you do not need to create a frame. The constraints may be a little strange. The first puts the view in the centre of the superview. The second gives it a width of 200. The next method is the vertical constraint which puts the view at the bottom and makes it 2 high.
UIView *sView = [[UIView alloc] init];
[sView setTranslatesAutoresizingMaskIntoConstraints:NO];
[superView addSubview:sView];
[superView addConstraint:[NSLayoutConstraint constraintWithItem:sView
attribute:NSLayoutAttributeCenterX
relatedBy:NSLayoutRelationEqual
toItem:superView
attribute:NSLayoutAttributeCenterX
multiplier:1.0
constant:0]];
[superView addConstraint:[NSLayoutConstraint constraintWithItem:sView
attribute:NSLayoutAttributeWidth
relatedBy:NSLayoutRelationEqual
toItem:Nil
attribute:NSLayoutAttributeWidth
multiplier:1.0
constant:200]];
[superView addConstraints:[NSLayoutConstraint
constraintsWithVisualFormat:#"V:[sView(2)]|"
options:0
metrics:nil
views:NSDictionaryOfVariableBindings(sView)]];

How do I stop an NSView subview being resized with Auto Layout

I am having a problem with the new Auto Layout functionality.
I add an NSView called tableView as a subview of workingBox. workingBox also contains some other subviews that were added with Interface Builder
[self.window setFrame:frame display:YES animate:YES];
[workingBox addSubview:tableView];
My problem is that if any of the other subviews touch tableView then they will get resized in the animation. The subviews that are not near where tableView is being added remain the correct size. I have already tried setting NSViewNotSizable resizeMask on the subviews that are getting resized but it does not work. The only thing that works is moving the subview upwards (into the wrong position.
EDIT: I have added a constraint in code:
NSDictionary *viewsDictionary = NSDictionaryOfVariableBindings(divider);
NSArray *constraints = [NSLayoutConstraint constraintsWithVisualFormat:#"V:[divider(==1)]"
options:0
metrics:nil
views:viewsDictionary];
[workingBox addConstraints:constraints];
This keeps the NSView at 1px height but if I lock it to the top it prevents the superview growing
I could not make this work by adding a constraint in code. However in Interface Builder I could see that there was a constraint for the NSView in question that was named 'Bottom Alignment'. I changed this to a priority of 1 in the inspector and it stopped the NSView getting stretched down when the window expanded. I think this auto added constraint that I mentioned in my comment above had been the problem all along.