Frame does not reflect auto-layout constraints after rejecting modal view controller

I use iOS 6, UIScrollView swap, and a clean machine.

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

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

Limitations for the scroll view are configured in IB. Here's how I set up restrictions for representing content in code:

 - (void)viewDidLoad { // ABPageScrollerContentView is a subclass of UIView; it overrides intrinsicContentSize; the size is calculated without referencing the scroll view 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 subviews are added/removed in the tilePages method (not shown); tilePages is called later in the view controller lifecycle... } 

If the user clicks the edit button, another view controller is presented using segue in the storyboard. After the view controller is rejected, the system seems to inexplicably modify the presentation frame of the content, even if the restrictions are not changed.

I rejected 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 do not understand how the x-component of the beginning of the frame has changed from 0 to -170. The restrictions are identical before and after the view controller rejects.

Here is the frame and limitations right before the rejectViewControllerAnimated method is called: completion ::

 (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 limitations after displaying the view controller:

 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 presentation frame change unexpectedly? And why does this not correspond to what is dictated by restrictions?

A pending call to hasAmbiguousLayout unexpectedly returns false. No exceptions are thrown. Scrolling even scrolls through scrolls, although the content view is partially off-screen.

No, where do I explicitly set the size of the scroll content; I leave this in the system. The content view has its own size (the size of the content view looks accurate, and the problem with the content presentation is a problem).

Resetting scroll content matches before and after rejecting the view controller. However, the offset of the x-component of the start of the content presentation is proportional to the content offset. The larger the content offset, the more negative the x component of the start of the content presentation after the modal presentation controller is rejected. And when the content is offset, the β€œzero” x component is zero. Therefore, if the modal presentation controller is presented when viewing the first page of the content (when the content offset is β€œzero”), the content presentation frame is correct when the view controller is fired. The only circumstance in which the content presentation frame correctly reflects its limitations is the case when the content offset is zero.

I tried inserting calls into layoutIfNeeded in different places without any results.

Any suggestions?

+5
ios autolayout uiscrollview
Sep 25 '13 at 5:57 on
source share
2 answers

I created a subclass of UIScrollView that works around this problem (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 
+7
Oct. 14 '13 at 23:26
source share

Since the only case where the content view frame was correct after the modal view controller was fired was when the scroll content offset was β€œzero”, I solved the problem as follows:

 // presenting view controller implementation self.contentOffsetBeforeModalViewControllerDismissal = self.pageScrollerView.contentOffset; self.pageScrollerView.contentOffset = CGPointMake(0, 0); [self dismissViewControllerAnimated:YES completion:nil]; 

Then I restored the actual content offset in the viewDidLayoutSubviews of the view controller:

 - (void)viewDidLayoutSubviews { if (!CGPointEqualToPoint(self.contentOffsetBeforeModalViewControllerDismissal, CGPointZero)) { self.pageScrollerView.contentOffset = self.contentOffsetBeforeModalViewControllerDismissal; self.contentOffsetBeforeModalViewControllerDismissal = CGPointZero; } } 

As it turned out, I was not able to restore the content bias in the viewAppear viewing view: because it's too early. Recovering content bias in viewDidAppear: It was too late because it created a sharp user interface upgrade. When working with automatic layout, I found that most often viewDidLayoutSubviews is suitable in terms of time.

It seems a bit hacked; but this is a familiar hack: save a bit of state, let the system do something, restore that bit of state.

Unfortunately, another error popped up quickly. If I submitted and rejected the modal view controller, scroll to another page, and then again present the modal view controller, the application will crash when the modal view manager is fired. The console did not report why the application might have been broken. An exception was thrown when the rejectViewControllerAnimated: function was called: termination :.

This last issue was resolved by disabling my custom pagination method (which runs anytime when the scroll content offset is changed). The pagination method allows you to display the correct content pages and process subroutines. I do not need to paginate my content-offset-dance, because the correct subqueries are already loaded. So, here is the final fix for rejecting a modal view controller without shifting the content presentation frame or crashing the application:

 // presenting view controller implementation self.disablePageTiling = YES // flag causes tilePages to do nothing self.contentOffsetBeforeModalViewControllerDismissal = self.pageScrollerView.contentOffset; self.pageScrollerView.contentOffset = CGPointMake(0, 0); // triggers tilePages, but nothing will happen because of the flag [self dismissViewControllerAnimated:YES completion:^{ self.disablePageTiling = NO; }]; 

The implementation of viewDidLayoutSubviews remains the same as above.

I do not believe that these problems will be completely resolved by any means. When using the automatic layout with scroll view, I constantly remain with this grumbling feeling that I am writing bad code. Therefore, I welcome further understanding. I am also wondering if iOS 7 will affect these issues.

0
Sep 26 '13 at 0:18
source share



All Articles