4

I'm using a header (supplementary view) in my UICollectionView. On the header I have label which has a certain distance from the left side. I'm doing my calculation and setting the constant of a constraint. Everything does work as expected.

If the orientation is now changed, the labels have the old position. Now I need to update the constraints for all my headers. Calling invalidateLayout doesn't update the constraints. How can I manually trigger the recalculation?

Edit:

This is how my layoutSubviews does look like, where the recalculation takes place:

public override void LayoutSubviews ()
{
    this.width = this.collectionViewSize.Width;
    this.itemWidth  = (nfloat)Math.Round(this.width / numberOfItemsInRow);

    leftSpacing.Constant =  this.itemWidth * this.referencePoint;
    if (leftSpacing.Constant == 0)
        leftSpacing.Constant = SectionHeader.MIN_SPACING;

    base.LayoutSubviews ();
}

As you can see this is not Objective-C but you should be able to see what I'm doing. I take the width of the collection view (set on instantiation of the supplementary view) and then I calculate the width of cell which corresponds nearly to the real size of the cells. With the referencePoint I determine the position. Here you can see the setup of my constraints:

public SectionHeader (CGRect frame) : base (frame)
{
    this.width = frame.Size.Width;
    this.itemWidth  = (nfloat)Math.Round(width / numberOfItemsInRow);

    label = new UILabel (){
        BackgroundColor = UIColor.White,
        TextAlignment = UITextAlignment.Left
    };

    label.TranslatesAutoresizingMaskIntoConstraints = false;
    AddSubview (label);

    NSMutableDictionary viewsDictionary = new NSMutableDictionary();
    viewsDictionary["label"] = label;

    this.AddConstraints(NSLayoutConstraint.FromVisualFormat("V:|[label]|",(NSLayoutFormatOptions)0,null,viewsDictionary));

    leftSpacing = NSLayoutConstraint.Create(label, NSLayoutAttribute.Left, NSLayoutRelation.Equal, this, NSLayoutAttribute.Left, 1, SectionHeader.MIN_SPACING);
    leftSpacing.Priority = 250;
    this.AddConstraint(leftSpacing);
    this.AddConstraint(NSLayoutConstraint.Create(label, NSLayoutAttribute.Right, NSLayoutRelation.Equal, this, NSLayoutAttribute.Right, 1, 0));
}

In this code the calculation for the distance from the left is nearly the same, except that frame has the height of the set headerReferenceSize. The width should be OK for the initialisation. I'm using the full height for the label, I set up the leading space and I pin it to the right.

testing
  • 19,681
  • 50
  • 236
  • 417

1 Answers1

2

You need to call layoutIfNeeded on the main view in the view controller so that the constraints are updated. Something like this should work:

- (void)willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration {
    // Update the constraints here for the new orientation - toOrientation
    // ...

    [UIView animateWithDuration:duration animations: ^{
        [self.view layoutIfNeeded];
    }];
}

This will animate the layout changes on orientation change.

You could add a breakpoint after the layoutIfNeeded call, to see that the frames are what you would expect.

If your constraints are not in the view controller, but in a UIView subclass, you can do the following:

- (void)awakeFromNib {
    [super awakeFromNib];
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(updateIntrinsicConstraints) name:UIDeviceOrientationDidChangeNotification object:nil];
}

- (void)updateIntrinsicConstraints {
    // Update your constraints here
    [self.constraint setConstant:newValue];

    // You might need to call [self layoutIfNeeded] here, if you don't call it in another place
}
- (void)dealloc {
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}

Let me know if this worked for you!


Update

You should not change the frame of the SectionHeader manually. You should do this by implementing the protocol method:

- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout referenceSizeForHeaderInSection:(NSInteger)section;

You could call layoutIfNeeded in the init method of the SectionHeader after you add the constraints to the label, add a breakpoint after to see if the frame is correct.

Let me know if it works out for you!

Catalina T.
  • 3,456
  • 19
  • 29
  • Thanks for your response. If I call `layoutIfNeeded` my current layout gets destroyed (cells width doesn't update). What does work here is if I call `invalidateLayout` after `layoutIfNeeded`. My next problem is that I don't have access to the constraints because they are in the `UICollectionReusableView` subclass. I only have access to the supplementary view on creation (in `collectionView:viewForSupplementaryElementOfKind:atIndexPath:`). – testing Mar 18 '15 at 12:10
  • I updated my answer to answer your question. The easiest way will be to extend `UICollectionReusableView` and in the subclass have an `IBOutlet` for the constraints you need to change and do this in the `updateIntrinsicConstraints` method – Catalina T. Mar 18 '15 at 13:27
  • I also thought something about that, but I added the notification only to a view controller and never to a subview. Would that bring performance penalties? I tried your approach and it does work! Because the constraints are adapted after the orientation changed I get auto layout errors (old width from landscape is used on portrait and exceeds the screen). Now I'm looking if I can catch the notification before the transition to the new orientation occurs ... – testing Mar 18 '15 at 15:04
  • 1
    On orientation change, `layoutSubviews` should be called for your custom view. Maybe you could update the constraints there depending on the current device orientation, so that they do not crash anymore. You just need to make sure you change the constraints first and call `[super layoutSubviews]` last. – Catalina T. Mar 18 '15 at 15:15
  • Thanks for the tip. Now I put everything into `layoutSubviews`, which does work without notification subscription. I'm still getting the auto layout error *Unable to simultaneously satisfy constraints*, because the label positioning (345) exceeds the screen size (UIView-Encapsulated-Layout-Width is 320). Do you have an idea why this still happens? I'm calling `[super layoutSubviews]` at last and the calculation should be correct. The encapsulated view seems to get updated after I changed my constraints, but he is complaining at that point I modify the constraint. – testing Mar 18 '15 at 15:40
  • 1
    Could you maybe post the constraint calculation in `layoutSubviews`? You should make sure there than the constraint value is never bigger than the width of the view. Another option would be to set the priority of one of your constraints (for example, the trailing space to superview) to Low (250). This way your constraints won't crash anymore and one the frame is set right for the orientation, the headerView will look right too. – Catalina T. Mar 18 '15 at 16:11
  • I edited my question and posted my constraint setup. I know that the constraint value shouldn't be bigger than the view, but when should the constraint be updated then? Now I set the priority to low as you said and it works like a charm! – testing Mar 19 '15 at 07:49
  • I don't get the auto layout error anymore. But it isn't completely correct. The supplementary view, which is visible on the screen isn't on its correct position after orientation change. All not visible supplementary views get the current collection view size set at initialisation. Do you have an idea for this? OK I saw that you edited your question. I did it for mine too to reflect the current state. I'll have a look into this. One more thing: on iOS 7 I've to set the priority to 750. Otherwise if the *leftSpacing* is nearly zero, it is pinned to the right side. – testing Mar 19 '15 at 09:04
  • I'm not changing the frame for *SectionHeader*. The frame is set in `UICollectionViewFlowLayout` with a *width = 0*. Using another width doesn't really change something. The only thing which won't work for me is that the supplementary view which is currently on screen isn't updated. So here I would need a logic which detects orientation change and only changes the calculation for the on screen view. Don't know how such a solution would look like. Because of other flaws of my design I changed my approach completely. Now I got what I wanted. I really appreciate your efforts in helping me! – testing Mar 19 '15 at 13:46