Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Frame doesn't reflect auto layout constraints after dismissing modal view controller

I'm using iOS 6, a paging UIScrollView, and pure auto layout.

Summary: I created a view controller that scrolls pages of content. Some of the views are created and configured in storyboard, others programmatically. Here's the view hierarchy:

- Main view (storyboard) 
  - UIScrollView (storyboard)
    - content view (programmatically)
      - subviews representing pages of content (programmatically)

The constraints for the scroll view are configured in IB. Here's how I configured the constraints for the content view in code:

- (void)viewDidLoad
{
   // ABPageScrollerContentView is a subclass of UIView; it overrides intrinsicContentSize; the size is calculated without referencing the scroll view's dimensions
   self.contentView = [[ABPageScrollerContentView alloc] init];
   self.contentView.translatesAutoresizingMaskIntoConstraints = NO;
   [self.pageScrollerView addSubview:self.contentView];

   // configure constraints between scroll view and content view...
   UIView *contentView = self.contentView;
   NSDictionary *viewsDictionary = NSDictionaryOfVariableBindings(contentView);

   [self.pageScrollerView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[contentView]|" options:0 metrics:0 views:viewsDictionary]];
   [self.pageScrollerView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[contentView]|" options:0 metrics:0 views:viewsDictionary]];

   // the content view's subviews are added/removed in the tilePages method (not shown); tilePages is called later in the view controller lifecycle...
}

If the user taps an edit button, another view controller is presented modally using a segue in the storyboard. After the view controller is dismissed, the system appears to inexplicably modify the frame of the content view even though the constraints are unchanged.

I dismiss the presented view controller in the following delegate method:

- (void)didExitEditPageViewVC:(id)controller
{
   // update currently displayed page view from data model...

   // logged content view frame = (0, 0; 1020, 460)

   [self dismissViewControllerAnimated:YES completion:^{

      // logged content view frame = (-170, 0; 1020, 460)
   }];
}

I don't understand how the x component of the frame's origin changed from 0 to -170. The constraints are identical before and after dismissing the view controller.

Here is the frame and the constraints right before calling the dismissViewControllerAnimated:completion: method:

(lldb) po self.contentView
$0 = 0x1ede2b40 <AEBPageScrollerContentView: 0x1ede2b40; frame = (0 0; 1020 460); layer = <CALayer: 0x1edd6f00>>

(lldb) po self.pageScrollerView.constraints
$1 = 0x1ed076c0 <__NSArrayM 0x1ed076c0>(
<NSLayoutConstraint:0x1ede2980 H:|-(0)-[AEBPageScrollerContentView:0x1ede2b40]   (Names: '|':UIScrollView:0x1edd3410 )>,
<NSLayoutConstraint:0x1eded480 H:[AEBPageScrollerContentView:0x1ede2b40]-(0)-|   (Names: '|':UIScrollView:0x1edd3410 )>,
<NSLayoutConstraint:0x1edecbc0 V:|-(0)-[AEBPageScrollerContentView:0x1ede2b40]   (Names: '|':UIScrollView:0x1edd3410 )>,
<NSLayoutConstraint:0x1ede1040 V:[AEBPageScrollerContentView:0x1ede2b40]-(0)-|   (Names: '|':UIScrollView:0x1edd3410 )>
)

Here is the frame and the constraints after the presenting view controller re-appears:

contentView = <AEBPageScrollerContentView: 0x1ede2b40; frame = (-170 0; 1020 460); layer = <CALayer: 0x1edd6f00>>

self.pageScrollerView.constraints =
(
    "<NSLayoutConstraint:0x1ede2980 H:|-(0)-[AEBPageScrollerContentView:0x1ede2b40]   (Names: '|':UIScrollView:0x1edd3410 )>",
    "<NSLayoutConstraint:0x1eded480 H:[AEBPageScrollerContentView:0x1ede2b40]-(0)-|   (Names: '|':UIScrollView:0x1edd3410 )>",
    "<NSLayoutConstraint:0x1edecbc0 V:|-(0)-[AEBPageScrollerContentView:0x1ede2b40]   (Names: '|':UIScrollView:0x1edd3410 )>",
    "<NSLayoutConstraint:0x1ede1040 V:[AEBPageScrollerContentView:0x1ede2b40]-(0)-|   (Names: '|':UIScrollView:0x1edd3410 )>"
)

Why did the content view's frame change unexpectedly? And why doesn't it match what is dictated by the constraints?

A delayed call to hasAmbiguousLayout returns false surprisingly. No exceptions are thrown. The scroll view even scrolls, albeit the content view is partly off-screen.

No where do I explicitly set the scroll view's content size; I leave that to the system. The content view has an intrinsic size (the content view's size appears to be fine; it's the content view's origin that is the problem).

The scroll view's content offset is the same before and after dismissing the view controller. However, the displacement of the x component of the content view's origin is proportional to the content offset. The greater the content offset, the more negative the x component of the content view's origin is after the modal view controller is dismissed. And, at a content offset of "zero", the x component is zero. So if the modal view controller is presented while viewing the first page of content (when the content offset is "zero"), the content view's frame is correct upon dismissal of the view controller. The content-offset-of-zero case is the only circumstance in which the content view's frame correctly reflects its constraints.

I have tried inserting calls to layoutIfNeeded in various places with no results.

Any suggestions?

like image 440
bilobatum Avatar asked Sep 25 '13 05:09

bilobatum


1 Answers

I created a UIScrollView subclass that works around this issue (which BTW is fixed in iOS7):

@interface ConstraintsSafeScrollView : UIScrollView
@end

@implementation ConstraintsSafeScrollView {
  CGPoint _savedContentOffset;
  UIEdgeInsets _savedContentInset;
}

- (void)willMoveToWindow:(UIWindow *)newWindow {
  if (newWindow) {
    // Reset the scrollview to the top.
    [super setContentOffset:CGPointMake(-_savedContentInset.left, -_savedContentInset.top)];
  }
  [super willMoveToWindow:newWindow];
}

// Overridden to store the latest value.
- (void)setContentOffset:(CGPoint)contentOffset {
  _savedContentOffset = contentOffset;
  [super setContentOffset:contentOffset];
}

// Overridden to store the latest value.
- (void)setContentInset:(UIEdgeInsets)contentInset {
  _savedContentInset = contentInset;
  [super setContentInset:contentInset];
}

- (void)didMoveToWindow {
  if (self.window) {
    // Restore offset and insets to their previous values.
    self.contentOffset = _savedContentOffset;
    self.contentInset = _savedContentInset;

  }
  [super didMoveToWindow];
}

@end
like image 171
Ian Wilkinson Avatar answered Dec 01 '22 11:12

Ian Wilkinson