Autolayout Constraints "disappear" on scroll in UIPageViewController - ios

I am trying to build a scroll view with a "floating" header using auto-layout. To be more exact I am trying to build a calendar view with several columns. Each of those column should have its own header which should float on top while the column can be scrolled vertically below it.
If everything works it looks like this:
As you can see there are several columns and a page control to indicate how many more pages of columns are available.
However when swiping/panning (or even just trying to swipe) to switch to the next page, the constraints keeping the header labels on top are removed and they disappear to the top of the scroll view (where they are in the storyboard).
Column headers are not visible due to being scrolled offscreen
Column headers are positioned at top of the scroll view (and the height is wrong).
Setup
A user can switch between dates ("Today" button at the top with left-right-arrows) and switch between the people displayed. (swipe/pan on view)
Boh interactions are realized with UIPageViewControllers inside one another. The outer page view controller switches between dates, the inner one between the pages of columns (with people).
The time view is contained in the outer page view controller but not the inner one.
So the hierarchy of views and controllers looks like this:
Calendar PageViewController (the one controlled via buttons in the navigation bar)
-- Scroll View of Page View Controller
---- Team View Controller (with view)
------ Header View reference (see screenshot 2)
------ Scroll View for vertical scrolling
-------- Time View (the one on the left)
-------- People PageViewController (the one controlled by swiping left/right)
---------- ScrollView of Page View Controller
------------ ViewController for a Single Page (with view)
--------------(1-n) Container View Controller (several of those, in the example 4 per page)
---------------- Column View Controller
------------------ Header View (A UIVisualEffectsView with a label)
------------------ calendar column view (the one doing the horizontal stripes)
Two pin the header views of the individual column view controllers to the top I use a reference view outside of the inner page view controller and outside of the vertical scroll view. I called it Header View reference in the overview above and you can see it quite nicely in the broken example:
It's a simple UIVisualEffectsView that I constrained to be at the top left with a fixed height and same width as the time view. So this view has the correct position and height I want all my header views to have and I use that to constraint the individual column header views to it in code (all other constraints are set up in storyboards) in the updateViewConstraints method of each ColumViewController like so:
- (void)updateViewConstraints
{
DDLogDebug(#"updating view constraints in colum view controller for %#", self.employee.displayName);
UIView *baseView = self.headerViewReferenceContainer.viewForHeaderContainerViewConstraints;
UIView *containerReference = self.headerViewReferenceContainer.headerContainerViewReference;
NSParameterAssert([containerReference isDescendantOfView:baseView] && [self.headerContainerView isDescendantOfView:baseView]);
[baseView addConstraint:[NSLayoutConstraint constraintWithItem:self.headerContainerView
attribute:NSLayoutAttributeTop
relatedBy:NSLayoutRelationEqual
toItem:containerReference
attribute:NSLayoutAttributeTop
multiplier:1.0
constant:0]];
[baseView addConstraint:[NSLayoutConstraint constraintWithItem:self.headerContainerView
attribute:NSLayoutAttributeHeight
relatedBy:NSLayoutRelationEqual
toItem:containerReference
attribute:NSLayoutAttributeHeight
multiplier:1.0
constant:0]];
[super updateViewConstraints];
}
baseView is the view to which the constraints should be added. It must be a superview of both the header reference view and the header view of the column view controller. At the moment this will be the view of the Team View Controller above (the one containing the vertical scroll view).
containerReference is the Header View Reference view as seen in the screenshot above.
So I constrain the column header views to have the same top position and same height as the reference view. (the width and x position depends on the column)
As you can see in the broken screenshots the Header View reference is always positioned correctly. Also, on loading the view controllers and there views, the constraints are setup correctly.
Then when starting a pan gesture which switches the page of the PeoplePageView Controller and loads the next ViewController for a Single Page the constraints somehow disappear and the header views move to the top of the scroll view due to no longer being pinned to the top.
The next View Controller for a Single Page has the constraints set up correctly, but only until it snaps into place.
So while panning the constraints of the view controller to display are setup correctly. But as soon as it snaps into place (feels like it happens at -viewDidAppear) the constraints are removed as well.
I don't understand why and how the constraints are removed.
What is not wrong / what I tried
It could be due to one of the related views disappearing, but
the baseView doesn't change at all on swiping left right as it is a superview of the paging scroll view
the header reference view is not changed as it is not contained in the paging Page View Controller.
the header column view does not disappear until the view controller is fully offscreen, so the layout should not break during the paging
I used to have an issue with the layout only falling into place AFTER the paging finished. This was due to my constraints relating to the top layout constraints as described in iOS 8 UIPageViewController Applying Constraints After Transitions.
As a result I use a fixed distance from the super view's top to pin my Header View Reference instead of pinning it to the top layout constraint.
So the bug of the UIPageViewController not having a correct top layout constraint on paging shouldn't be the issue either.
I do not add any views programatically. All my Storyboards use auto layout and view controller are added to each other via Container View Controllers in the Storyboard or implementing a UIPageViewControllerDatasource and instantiating the view controller from a xib. So there shouldn't be any issues related to translatesAutoResizingMasks. Also, it works on load, it only breaks on paging, this doesn't fit the usual translatesAutoResizingMasks = YES problem.
I also do not get any errors about conflicting constraints or the view hierarchy not being prepared for constraints, the working constraints are simply removed.
I due have one issue which I think is unrelated, but I'll list it here for completeness' sake: I get an error due to the layout constraints of the labels inside the column header views being added to early:
The view hierarchy is not prepared for the constraint: <NSLayoutConstraint:0x7af4df20 UILabel:0x7af4e0d0'Susanne'.width == UIVisualEffectView:0x7af4de70.width>
When added to a view, the constraint's items must be descendants of that view (or the view itself). This will crash if the constraint needs to be resolved before the view hierarchy is assembled. Break on -[UIView _viewHierarchyUnpreparedForConstraint:] to debug.
However, that message complains about the relation between the label and its containing view (the header view of the column), not about the "outer" constraints of the header view. Also, it warns that the app will crash if the constraints need to be resolved too early. The app does not crash, so the timing seems to be right. Also, the constraints the app complains about are setup in a storyboard, so I don't see how I could get the timing wrong.
tl;dr: Constraints that reach through several levels of the view hierarchy from inside a UIPageViewController to a view outside of it are removed (without warning, comment, reason or anything).
I don't understand why and how to prevent that from happening or at what point to add them again.
UPDATE:
I added the following call to viewWillAppear::
[self.view setNeedsUpdateConstraints]
This has the following effect:
When dragging the page to the side a bit (not switching the paging) and releasing it again (so it snaps back) the broken constraints are repaired again (and the layout is fixed). This is due to the page view controller calling viewWillAppear: on the original view controller if it "snaps back" (to offset the previously issued viewWillDisappear call).
But during the scrolling the layout is still broken.
Also when swiping to a new page, the constraints of the new page are also broken.
But whenever viewWillAppear is called the constraints are fixed for a short time until they are removed again (usually when the scroll ends).
If I add the same call to viewDidAppear instead of viewWillAppear the following happens:
This leads to the layout being correct most of the time (they are fixed after scrolling ends).
But it is still broken during scrolling.
Now if I add [self.view setNeedsUpdateConstraints] to both methods the following happens:
The layout behaves correctly almost all of the time, no strange jumping around of views
Except for the first page change. During that one the constraints of the original page seem to be removed and the layout is broken in the way shown above. After that, the constraints seem to be fixed permanently and swiping back and forth works without issues.
This is even true for three pages (I also tried it with four) when paging to the last page (and trying to go beyond it) and back although I'm pretty sure the page view controller does not keep all three pages in memory.

Is there a chance that updateViewConstraints is being called more than once causing multiple additions of the same constraints which is then causing an issue. I believe this can happen quite easily.
I notice in numerous discussions that many seem to recommend not doing the create and add in this function, (viewDidLoad seems preferred) and to only do the constraint value settings in this function to minimize the issue of multiple calls but to ensure the sizes are correct. Another option seems to be to put a Boolean guard for the controller around constraint addition to make sure it only occurs once in the lifetime of the controller.
Might not be your issue, but thought worth mentioning.

Try setting your constraints in viewDidLayoutSubviews. I am not quite certain but I remember solving a constraint related issue this way and it seemed to work.

Why don't you set the top of the headerView to the top of the baseview. i.e. the view that contains the scrollview. This will fix your issue for sure.

Ok, I still think it is a bug, but I found a workaround:
I basically have two constraint I want to keep and which sometimes disappear. I create properties for these, but make them weak:
#property (weak) NSLayoutConstraint *headerViewHeightConstraint;
#property (weak) NSLayoutConstraint *headerViewTopConstraint;
I then assign the created constraints to these properties in updateViewConstraints like this:
NSLayoutConstraint *topConstraint = [NSLayoutConstraint constraintWithItem:self.headerContainerView
attribute:NSLayoutAttributeTop
relatedBy:NSLayoutRelationEqual
toItem:containerReference
attribute:NSLayoutAttributeTop
multiplier:1.0
constant:0];
[baseView addConstraint:topConstraint];
self.headerViewTopConstraint = topConstraint;
By using a weak reference I can check whether the constraint exists without keeping it around if nothing else references it. This way, when the constraint is removed from the view (for whatever reason) it will be deallocated an my property will be nil. I can use that to check whether I need to reestablish the constraint like this:
- (void)reestablishHeaderViewConstraintsIfNecessary
{
if (!self.headerViewTopConstraint || !self.headerViewHeightConstraint) // if any of the constraints is missing
{
[self.view setNeedsUpdateConstraints];
}
}
Now I just need a place to call that method from, to make sure it is called whenever the constraints might have been removed. As described in the question viewWillAppear and viewDidAppear are not good enough. Instead I use viewWillLayoutSubviews. This is called whenever the bounds change (e.g. many times during scrolling), which is quite often, but because I wrap the actual constraint invalidation this should be cheap enough if nothing needs to be done:
- (void)viewWillLayoutSubviews
{
[self reestablishHeaderViewConstraintsIfNecessary];
[super viewWillLayoutSubviews];
}
I think it is important to use viewWillLayoutSubviews, not viewDidLayoutSubviews ad invalidating the layout in viewDidLayoutSubviews leads to an exception but I'm not sure. This is also the reason why I put the call in front of the call to [super viewWillLayoutSubviews], but I didn't try whether it would work with the call appearing after it.

Related

How to shift UIView's and put a new UIView programmatically with Swift?

I've a layout with my data, but depending on data I need change the location of views. I wants put a UIView (UILabel, UITextField...) at top of screen and shift the layout to down, but I don't know how to do this, I saw some docs and tutorials but not helps me. I made my screen with storyboard.
This is original screen:
This is that I want:
My storyboard:
If you just want to add the view at the top of your stack view, use stackView.insertArrangedSubview(newView, at:0)
I would suggest setting up your screen with your "put other view here" in place, and a fixed height constraint on that view. Make the views below have a fixed space to the bottom of the "other view here view."
Then control-drag from the "put other view here"'s height constraint into your view controller. This will make the constraint an outlet.
Now edit the height constraint and set it's constant to 0. This will collapse that view to 0 height, causing it to disappear, and the other views to shift up.
Then, in code, when you want the "other view here" view to show up, use the outlet to set the constant on that view back to it's previous non-zero value, and then call layoutIfNeeded() on the parent view to update the view layouts based on changed constraints.
You could also write code that would insert your "other view here" view into the view hierarchy, but that would involve removing constraints and adding constraints in code, which is a bit of a pain.
By adjusting the height constraint on the view you can show/hide it back and forth with very little effort.

How do I prepare a UIViewController's view for being added to a UIStackView?

This is what I'm trying to do...
I have one view controller that needs to dynamically display different subviews based on the presence of some data.
Here is a simple mockup. Each colored block represents a unique subview.
Sometimes the green block needs to be at the top, sometimes the green block won't display at all, sometimes the light blue block will be something different, etc.
Each subview has interactive elements, so I've been creating and adding them like so:
Defining a new view controller
Defining its view
Calling addChildViewController and didMoveToParentViewController
Calling addSubview on myNewViewController.view
Using SnapKit to make auto layout constraints to position the view
I want to transition to UIStackView because it seems a good support system for this view because all I need to do is stack its subviews. I'm seeing many conflicting constraint errors and unexpected view frames when trying to add subviews with their own inner auto layout constraints.
Question
Am I setting myself up for failure here by embedding the views of 4-6 view controllers in the view of one view controller?
Also, how do I give the added views properties like minimum heights or content sizes without seeing many breaking constraints with UIStackView? (So they can stack, but one of them is say, 400 tall, and the other is 200 tall)
Answer
You can absolutely do this using UIContainerViews combined with UIStackViews and UIScrollViews, it's a complicated setup so here's a starter project to get you started:
https://github.com/Rnorback/ScrollingStackView
Here's what that project looks like:
You want the containers to have different sizes. In order to do that, simply add height constraints to the containers with Autolayout. If you want to change the height of the scrolling. Then you'll need to change the height constraint of the UIStackView.
Brief Explanation
When creating this setup, you have to make sure the UIStackView distribution setting stays in fill. That way you can set height constraints on UIContainerViews.
When setting up anything in a UIScrollView you have to make sure that the object is pinned to the edges of the scroll view and has a defined width and height, otherwise the scrollview will throw a constriant error. I think of it like the scrollview tries to press in on all sides of your content view and if any side gives, the scrollview won't be able to present it properly.
Hope this helps.

hide a view and remove blank spaces

I'm developing an app for iOS and I'm using the Storyboard with AutoLayout ON. One of my view controllers has a set of 3 labels, and in certain circumstances i would like to make the second one disappear.
If I use the setHidden:TRUE method the label become invisible but it still obviously take space in the view.
can someone point me to the right direction?
The simplest solution is to put the views that you want to hide inside a StackView. Then to hide the element simply make it hidden:
_myElement.hidden = YES;
StackView will squash hidden elements and they will become invisible.
I think you can link the constraint with the header file of your viewController. Then modify the constraint and commit changes.
Edited:
1) Create the IBOutlet for the constraint.
2) Modify the constraint, for example: self.yourConstraint.constant = 0.0;
3) Commit the new constraint: [viewForUpdate setNeedsUpdateConstraints];
The easiest and most effective way to handle this is using Stack Views. Insert the label in a horizontal/vertical (orientation they appear on your UI) stack view and stack view will internally take care of the spacing. Additional properties like alignment, spacing can be tweaked as per requirement. Make sure you re-establish the constraints between stack view and adjacent elements because once the views are added to a stack view all if its constraints are cleared
You will need to move the other views by adjusting their frames. This can be done directly, or if using auto layout, by giving them a vertical spacing constraints to the view being hidden.
If there are many other views that depend on the hiding/showing view, create another subview that contains all of the dependent views. The dependent views can layout statically on that parent, and that parent can have it's frame adjusted (again, either directly or via auto layout).
view
|
--- view to hide
|
--- common parent (move this with auto layout or directly)
|
--- subview's with position dependent on view to hide
--- ...
This is a late answer/solution, but I have just built a category which does just that - hiding the view without blank spaces.
https://github.com/neevek/UIView-Visibility

AutoLayout or not? How to position 3 subviews?

I can't figure out how to correctly position subviews in a Navigation Controller.
I am trying to position a view, table and another view.
If I turn off AutoLayout than top view and table are ok but my bottom view is pushed off the screen.
With autolayout I get both the table and bottom view in the wrong place:
I try to set frame in viewDidLoad as follows (calendarPicker is position at the top below navigation bar), I want the configPanel to be on the bottom, I hide bottom bar on Push.
self.eventsTable.frame = CGRectMake(0, CGRectGetMaxY(self.calendarPicker.frame),
self.eventsTable.bounds.size.width,
self.view.bounds.size.height - self.calendarPicker.bounds.size.height
);
self.configPanel.frame = CGRectMake(0, self.view.bounds.size.height,
self.configPanel.bounds.size.width,
self.view.bounds.size.height - self.configPanel.bounds.size.height
);
Should I rely on autolayout? How should I make my constraints?
I believe my problems arise due to autosizing of the table mostly, but given that I am setting its frame size why would it not change? I do not want to remove auto layout since it is used on other views designed in the storyboard and from what I understand it applies to all?
Is there something that I am missing that needs to be done on top of setting the frames of individual controls?
EDIT:
I think my biggest problem is autolayout and inability to size table appropriately. If I add constraint to the bottom view to be 0 from bottom of the view, it will originally appear correctly. However consequently when resizing table and top view, the table will push bottom view down sizing itself to occupy all available space.
I need to force UITableView to be no more than height between the bottom of the top view and lower view, but still not sure how to do this.
Somewhat closer
Removing code for frame change of the table fixes the issue of the bottom view being pushed off. However in this case top view overlapps table when it changes size at the same time not being drawn correctly:
Uncheck autolayout and then set frames of all three subviews.
Or
If you want constraints then you can use NSLayoutConstraint class to add constraint to your subviews.
I recommend against switching off autolayout, especially because you can't do it on a view by view basis. Autolayout is a must if you tend to do i18n or want to make sure your app does well in different screen resolutions/orientation.
Try This!
disable use Autolayout in storyboard. Place your three views on view controller(view1, view2, view3). Next go to size inspector and use autosizing masks for all three views. Check this, which will help you
http://www.raywenderlich.com/50317/beginning-auto-layout-tutorial-in-ios-7-part-1

Autolayout how to get computed value from view

There is one thing that is really making me crazy about auto layout , I'm doing my tests and found that if you subclass a UIView and you put some views with their constraints is impossible to know the computed value.
Let't say that we have a TestViewClass that inherits from UIView, we've got some subviews inside. we want to use it for iPhone and iPad, so we use in two sizes, let's suppose 100x100 and 200x200 we made our constraints to make everything work. We build this view placing those subviews in some positions.
Now we need to build another subviews that contains a number of buttons as subviews (contentView) which number is given at runtime.
The algorithm will pick the size of this content view and put the correct number of buttons calculating the space between them in a way that the will be at the same distance from each other but covering the whole content view width. For example, the contentView is 200 point in width, buttons are 3 and squared with a side of 60. Together they cover 180, so we have 20 points left that should be placed as a space between the buttons->10 points.
That's pretty easy, made thousand of times before auto layout. To do that I need the width of the contentView which has some constraints to its superview that make it resize its width according to superview size, the problem is that I can't find any place inside UIView implementation where I can get the final value of the size of the contentView.
I tried view -layoutSubviews, -updateConstraints, -didMoveToSuperview, value show are alway the original one. When the new frames are calculated?
I clearly didn't get something about auto layout...
I discovered this problem trying to set table view cell height, trying to make them appear all on screen independently by the size of the table view. here is the related question other question
Finally I've get what I was missing about auto layout and it is a fundamental concept.
Autolayout doesn't work like autoresizing masks, the new frames aren't
updated instantaneously but by request.
This makes a huge difference, it seems to be in sync with the CATransaction render cycle, that makes sense, because layout calculation could be an expensive task and is useless to do until you really need it, most of the time during rendering.
Of course there are few exceptions, like this one and how can we force auto layout to make this calculation? we can do setting -setNeedsLayout method and -layoutIfNeeded. the first method marks the view as "dirty" and the second forces an immediate layout. If you set call those two methods in -didMoveToSuperview right after you get correct updated frames. Hope this helps,
Andrea
You can use view controller's viewDidLayoutSubviews, which will notify you when the views are laid out. If all of this is adding of constraints is happening in a custom UIView, you could simply write a method for your view called viewDidLayoutSubviews and have the view controller call that when the view controller receives it. At this point, the containers should have their dimensions properly configured.
But, there are a couple of approaches of even spacing a bunch of buttons without needing to bother knowing the size of the container view:
One approach I've used for even spacing controls in a container is to create what I call "spacer" views (views that are present, but just have a clearColor background, so you can't visually see them). You can then create a series of constraints using, effectively, something like:
H:|[spacer1][button1(60)][spacer2(==spacer1)][button2(60)][spacer3(==spacer1)][button3(60)][spacer4(==spacer1)]|
If your number of buttons is fixed, you can do this in a single constraintsWithVisualFormat, like above. If it's a variable number of buttons, you can iterate through them, building a VFL string for each pair of button and spacer (e.g. the first button would use VFL of
H:|[spacer][button(60)]
all the subsequent ones would create another button and another spacer and use the following VFL:
H:[previousButton][spacer(==originalSpacer)][button(60)]
and then I add one final spacer at the end:
H:[lastButton][spacer(==originalSpacer)]|
This is a little cumbersome, but as you can see, your controls are perfectly laid out, evenly spaced, and you never needed to know the dimensions of the container.
Another approach is to use the NSLayoutFormatAlignAllCenterX, attribute. This bypasses the need to create all of those spacers, but if you use a simple algorithm like I do below, the centers will be evenly distributed amongst themselves. Thus, in addition to the standard vertical constraints and the horizontal width constraint for the button, you can then control the horizontal placement of the buttons with:
[containerView addConstraint:[NSLayoutConstraint constraintWithItem:button[i]
attribute:NSLayoutAttributeCenterX
relatedBy:NSLayoutRelationEqual
toItem:containerView
attribute:NSLayoutAttributeCenterX
multiplier:2.0 * (double) (i + 0.5) / (double) n
constant:0.0]];
where i is the zero-based index of which button you're setting this horizontal constraint for, and n is the number of buttons.

Resources