Autolayout and Constraints for iPad - objective-c

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:

Related

UIVIew from XIB with Autolayout to UItableView Header

I am writing because I have a problem with the Auto Layout.
I'm trying to create a simple view in InterfaceBuilder with Auto Layout I want to load code and enter as a header of a table (not as header section). I explain briefly what are the characteristics.
The imageView must be square and must be as wide as the screen.
The space under the picture to the bottom of view that contains the button and label must be high 50 points.
Between image and button has to be a fixed distance of 12 points.
Between image and label must be a fixed distance of 13 points.
All these features are able to get them with Auto Layout. I added a constraint to the aspect ratio of the image (1: 1) and the various constraints for distances. all right.
The real problem is that by launching the app on iphone 6+ simulator (414 points of width), the image (with the label and button) goes above the cells.
Enabling various transparencies I noticed that the superView of Image View, only increase the width. It does not increase its height! How do I fix?
This is the code:
- (void)viewDidLoad{
//...
PhotoDetailsHeaderView *hView = (PhotoDetailsHeaderView *)[[[NSBundle mainBundle] loadNibNamed:#"PhotoDetailsHeaderView" owner:self options:nil] objectAtIndex:0];
hView.delegate = self;
self.tableView.tableHeaderView = hView;
//...
}
This is how I create the xib:
and this is how it is on the simulator, the green box is Uiimageview and the yellow box (under green box) is the mainview (or superview):
How can fix it?
Many thanks to all!
You'll need to add a property to store your PhotoDetailsHeaderView:
#property (nonatomic, strong) PhotoDetailsHeaderView *headerView;
Then calculate its expected frame in viewDidLayoutSubviews. If it needs updating, update its frame and re-set the tableHeaderView property. This last step will force the tableView to adapt to the header's updated frame.
- (void)viewDidLayoutSubviews{
[super viewDidLayoutSubviews];
CGRect expectedFrame = CGRectMake(0.0,0.0,self.tableview.size.width,self.tableView.size.width + 50.0);
if (!CGRectEqualToRect(self.headerView.frame, expectedFrame)) {
self.headerView.frame = expectedFrame;
self.tableView.tableHeaderView = self.headerView;
}
}
The problem is probably that in iOS you have to reset the header of the table view manually (if it has changed its size). Try something along these lines:
CGRect newFrame = imageView.frame;
imageView.size.height = imageView.size.width;
imageView.frame = newFrame;
[self.tableView setTableHeaderView:imageView];
This code should be in -(void)viewDidLayoutSubviews method of your view controller.

How to install a floating layout constraint programmatically

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.

Change size of viewing area after changing size of UINavigationBar and UITabBar

I'm currently working on my very first app and I've changed the size of the UINavigationBar and UITabBar and now I have extra space black space in the general viewing area (etc. ViewController, DetailViewController). How can I change the viewing area to accommodate for this new size?
I've pasted how I'm currently setting the new size for both UINavigationBar and the UITabBar.
/* Get the screenarea of the device */
CGRect screenArea = [[UIScreen mainScreen] applicationFrame];
/* Define the size of the navigation bar */
CGRect viewHeaderbBarFrame = navigationBar.frame;
viewHeaderbBarFrame.origin.y = screenArea.origin.y;
viewHeaderbBarFrame.origin.x = screenArea.origin.x;
viewHeaderbBarFrame.size.height = 44;
viewHeaderbBarFrame.size.width = screenArea.size.width;
navigationBar.frame = viewHeaderbBarFrame;
/* Define the size of the footer bar */
CGRect viewTabBarFrame = tabBar.frame;
viewTabBarFrame.origin.y = screenArea.size.height - 26;
viewTabBarFrame.origin.x = screenArea.origin.x;
viewTabBarFrame.size.height = 46;
viewTabBarFrame.size.width = screenArea.size.width;
tabBar.frame = viewTabBarFrame;
Thanks.
basically by shrinking your tabbar and navbar, you need your view to expand. I'm assuming that you are using a tabbar for primary navigation.
Figure out the proper size and adjust your view like this:
[[self.tabBarController.view.subviews objectAtIndex:0] setFrame:CGRectMake(newX,newY,newWidth,newHeight)];
in the buyer beware category, if your goal is the app store, you would be wise to review the guidelines on modifying those elements. I'm not sure of all the specific points, but I think apple frowns making changes like that to plainly displayed core navigation elements.
Also, this approach works well for the current screen dimensions, but may not work if the rumors are true and the next phone has a bigger screen.
be well.
You should never have to mess with the view height in this way. Views are pushed onto the nav bar. Their size should be set to fit a 320 x 480 screen minus tab, nav, and title bars. And you should set the AutoResize width/height as flexible and the left/rigth/top/bottom to not-flexible (just leave them out).
view.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight
iOS likes everything to to be spring loaded and pinned to one another. Then when you rotate things, it all sticks and moves properly. They do all the work for you. So, it's best to embrace their funky way of handing things. The interface builder gui can help. It even lets you test out rotation if you want. And you can set the option of tab/nav/title bars too. Or if you understand it, then you can also do this manually.

Infinite horizontal scrolling UIScrollView

I have a UIScrollView whose content size is 1200x480. I have some image views on it, whose width adds up to 600. When scrolling towards the right, I simply increase the content size and set the offset so as to make everything smooth (I then want to add other images, but that's not important right now). So basically, the images currently in the viewport remain somewhere on the left, eventually to be removed from the superview.
Now, the problem that I have happens when scrolling towards the left. What I do is I move the images to the end of the content size (so add 600 to each image view's origin.x), and then set the content offset accordingly. It works when the finger is on the screen and the user drags (scrollView.isTracking = YES). When the user scrolls towards the left and lets go (scrollView.isTracking = NO), the image views end up moving too fast towards the right and disappear almost instantly. Does anyone know how I could have the images move nicely and not disappear even when the user's not manually dragging the view and has already let go?
Here's my code for dragging horizontally:
-(void) scrollViewDidScroll:(UIScrollView *)scrollView {
CGPoint offset = self.scrollView.contentOffset;
CGSize size = self.scrollView.contentSize;
CGPoint newXY = CGPointMake(size.width-600, size.height-480);
// this bit here allows scrolling towards the right
if (offset.x > size.width - 320) {
[self.scrollView setContentSize:CGSizeMake(size.width+320, size.height)];
[self.scrollView setContentOffset: offset];
}
// and this is where my problem is:
if (offset.x < 0) {
for (UIImageView *imageView in self.scrollView.subviews) {
CGRect frame = imageView.frame;
[imageView setFrame:CGRectMake
(frame.origin.x+newXY.x, frame.origin.y, 200, frame.size.height)];
}
[self.scrollView setContentOffset:CGPointMake(newXY.x+offset.x, offset.y)];
}
}
EDIT: This is now working - I had a look at StreetScroller and it's all good now.
However, I now want to zoom in on the scrollview, but viewForZoomingInScrollView is never called. Is it not possible to zoom in on a scrollview with a large content size?
There are some approaches floating around here. Just use the site search …
If you want an more "official" example created by Apple take a look at the StreetScroller Demo. For some more information about this example take a look at last years WWDC session no. 104 Advanced Scroll View Techniques.
There is also an UIScrollView subclass on Github called BAGPagingScrollView, which is paging & infinite, but it has a few bugs you have to fix on your own, because it's not under active development (especially the goToPage: method leads to problems).

Synchronised scrolling between two instances of NSScrollView

I have two instances of NSScrollView both presenting a view on the same content. The second scroll view however has a scaled down version of the document view presented in the first scroll view. Both width and height can be individually scaled and the original width - height constraints can be lost, but this is of no importance.
I have the synchronised scrolling working, even taking into account that the second scroll view needs to align its scrolling behaviour based on the scaling. There's one little snag I've been pulling my hairs out over:
As both views happily scroll along the smaller view needs to slowly catch up with the larger view, so that they both "arrive" at the end of their document at the same time. Right now this is not happening and the result is that the smaller view is at "end-of-document" before the larger view.
The code for synchronised scrolling is based on the example found in Apple's documentation titled "Synchronizing Scroll Views". I have adapted the synchronizedViewContentBoundsDidChange: to the following code:
- (void) synchronizedViewContentBoundsDidChange: (NSNotification *) notification {
// get the changed content view from the notification
NSClipView *changedContentView = [notification object];
// get the origin of the NSClipView of the scroll view that
// we're watching
NSPoint changedBoundsOrigin = [changedContentView documentVisibleRect].origin;;
// get our current origin
NSPoint curOffset = [[self contentView] bounds].origin;
NSPoint newOffset = curOffset;
// scrolling is synchronized in the horizontal plane
// so only modify the x component of the offset
// "scale" variable will correct for difference in size between views
NSSize ownSize = [[self documentView] frame].size;
NSSize otherSize = [[[self synchronizedScrollView] documentView] frame].size;
float scale = otherSize.width / ownSize.width;
newOffset.x = floor(changedBoundsOrigin.x / scale);
// if our synced position is different from our current
// position, reposition our content view
if (!NSEqualPoints(curOffset, changedBoundsOrigin)) {
// note that a scroll view watching this one will
// get notified here
[[self contentView] scrollToPoint:newOffset];
// we have to tell the NSScrollView to update its
// scrollers
[self reflectScrolledClipView:[self contentView]];
}
}
How would I need to change that code so that the required effect (both scroll bars arriving at an end of document) is achieved?
EDIT: Some clarification as it was confusing when I read it back myself: The smaller view needs to slow down when scrolling the first view reaches the end. This would probably mean re-evaluating that scaling factor... but how?
EDIT 2: I changed the method based on Alex's suggestion:
NSScroller *myScroll = [self horizontalScroller];
NSScroller *otherScroll = [[self synchronizedScrollView] horizontalScroller];
//[otherScroll setFloatValue: [myScroll floatValue]];
NSLog(#"My scroller value: %f", [myScroll floatValue]);
NSLog(#"Other scroller value: %f", [otherScroll floatValue]);
// Get the changed content view from the notification.
NSClipView *changedContentView = [notification object];
// Get the origin of the NSClipView of the scroll view that we're watching.
NSPoint changedBoundsOrigin = [changedContentView documentVisibleRect].origin;;
// Get our current origin.
NSPoint curOffset = [[self contentView] bounds].origin;
NSPoint newOffset = curOffset;
// Scrolling is synchronized in the horizontal plane so only modify the x component of the offset.
NSSize ownSize = [[self documentView] frame].size;
newOffset.x = floor(ownSize.width * [otherScroll floatValue]);
// If our synced position is different from our current position, reposition our content view.
if (!NSEqualPoints(curOffset, changedBoundsOrigin)) {
// Note that a scroll view watching this one will get notified here.
[[self contentView] scrollToPoint: newOffset];
// We have to tell the NSScrollView to update its scrollers.
[self reflectScrolledClipView:[self contentView]];
}
Using this method the smaller view is "overtaken" by the larger view when both scrollers reach a value of 0.7, which is not good. The larger view then scrolls past its end of document.
I think you might be approaching this in the wrong way. I think you should be getting a percentage of how far down each scroll be is scrolled in relation to itself and apply that to the other view. One example of how this could be done is this way using NSScroller's -floatValue:
NSScroller *myScroll = [self verticalScroller];
NSScroller *otherScroll = [otherScrollView verticalScroller];
[myScroll setFloatValue:otherScroll.floatValue];
I finally figured it out. The answer from Alex was a good hint but not the full solution as just setting the float value of a scroller doesn't do anything. That value needs translation to specific coordinates to which the scroll view needs to scroll its contents.
However, due to differences in size of the scrolled document view, you cannot just simply use this value, as the scaled down view will be overtaken by the "normal" view at some point. This will cause the normal view to scroll past its end of document.
The second part of the solution was to make the normal sized view wait with scrolling until the scaled down view has scrolled its own width.
The code:
// Scrolling is synchronized in the horizontal plane so only modify the x component of the offset.
NSSize ownSize = [[self documentView] frame].size;
newOffset.x = MAX(floor(ownSize.width * [otherScroll floatValue] - [self frame].size.width),0);
The waiting is achieved by subtracting the width of the scroll view from the width times the value of the scroller. When the scaled down version is still traversing its first scroll view width of pixels, this calculation will result in a negative offset. Using MAX will prevent strange effects and the original view will quietly wait until the value turns positive and then start its own scrolling. This solution also works when the user resizes the app window.