关闭模式视图控制器后,框架不反映自动布局约束

Posted

技术标签:

【中文标题】关闭模式视图控制器后,框架不反映自动布局约束【英文标题】:Frame doesn't reflect auto layout constraints after dismissing modal view controller 【发布时间】:2013-09-25 05:57:58 【问题描述】:

我使用的是 ios 6、分页 UIScrollView 和纯自动布局。

总结:我创建了一个滚动内容页面的视图控制器。一些视图是在情节提要中创建和配置的,另一些是通过编程方式创建和配置的。这是视图层次结构:

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

滚动视图的约束在 IB 中配置。以下是我在代码中为内容视图配置约束的方式:

- (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...

如果用户点击一个编辑按钮,另一个视图控制器会在情节提要中使用 segue 以模态方式呈现。视图控制器关闭后,系统似乎莫名其妙地修改了内容视图的框架,即使约束没有改变。

我在以下委托方法中关闭呈现的视图控制器:

- (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)
   ];

我不明白框架原点的 x 分量如何从 0 变为 -170。解除视图控制器之前和之后的约束是相同的。

这是调用dismissViewControllerAnimated: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 )>
)

这是呈现视图控制器重新出现后的框架和约束:

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 )>"
)

为什么内容视图的框架发生了意外变化?为什么它不符合约束的规定?

对 hasAmbiguousLayout 的延迟调用令人惊讶地返回 false。不抛出异常。滚动视图甚至滚动,尽管内容视图部分在屏幕外。

没有我在哪里明确设置滚动视图的内容大小;我把它留给系统。内容视图具有固有大小(内容视图的大小似乎很好;问题在于内容视图的来源)。

滚动视图的内容偏移在关闭视图控制器之前和之后是相同的。但是,内容视图原点的 x 分量的位移与内容偏移量成正比。内容偏移量越大,模式视图控制器关闭后内容视图原点的 x 分量就越负。并且,在“零”的内容偏移量处,x 分量为零。因此,如果在查看内容的第一页时呈现模态视图控制器(当内容偏移量为“零”时),则在关闭视图控制器时内容视图的框架是正确的。内容偏移为零的情况是内容视图的框架正确反映其约束的唯一情况。

我尝试在不同的地方插入对 layoutIfNeeded 的调用,但没有结果。

有什么建议吗?

【问题讨论】:

【参考方案1】:

我创建了一个 UIScrollView 子类来解决这个问题(顺便说一句,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

【讨论】:

聪明的解决方案。我喜欢继承 UIScrollView 的想法。修复时易于移除。我已将我的项目升级到 iOS 7,但我已经转移到我项目的另一部分。当我返回此视图控制器时,我可以删除我的修复版本。【参考方案2】:

由于在模式视图控制器解除后内容视图的框架正确的唯一情况是滚动视图的内容偏移量为“零”时,我解决了以下问题:

// presenting view controller's implementation
self.contentOffsetBeforeModalViewControllerDismissal = self.pageScrollerView.contentOffset;

self.pageScrollerView.contentOffset = CGPointMake(0, 0); 

[self dismissViewControllerAnimated:YES completion:nil];

然后,我在呈现视图控制器的 viewDidLayoutSubviews 方法中恢复了实际的内容偏移量:

- (void)viewDidLayoutSubviews

    if (!CGPointEqualToPoint(self.contentOffsetBeforeModalViewControllerDismissal, CGPointZero)) 

       self.pageScrollerView.contentOffset = self.contentOffsetBeforeModalViewControllerDismissal;

       self.contentOffsetBeforeModalViewControllerDismissal = CGPointZero;
    

事实证明,我无法在呈现视图控制器的 viewWillAppear: 方法中恢复内容偏移量,因为它为时过早。恢复 viewDidAppear: 中的内容偏移为时已晚,因为它产生了不和谐的 UI 更新。在使用自动布局时,我发现 viewDidLayoutSubviews 在时间方面往往恰到好处。

这感觉有点像 hack;但这是一个熟悉的技巧:保存一点状态,让系统做它的事情,恢复那一点状态。

不幸的是,另一个错误很快浮出水面。如果我呈现和关闭模态视图控制器,滚动到另一个页面,然后再次呈现模态视图控制器,当模态视图控制器被解散时,应用程序将崩溃。控制台中没有提供有关应用程序可能崩溃的原因的信息。在调用dismissViewControllerAnimated:completion: 时抛出异常。

通过关闭我的自定义页面平铺方法(滚动视图的内容偏移量发生变化时触发)解决了后一个问题。页面平铺方法确保显示正确的内容页面并回收子视图。在我的内容偏移舞蹈期间,我不需要进行页面平铺,因为已经加载了正确的子视图。因此,这是在不替换内容视图的框架或使应用程序崩溃的情况下关闭模态视图控制器的最终修复:

// presenting view controller's 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;
]; 

viewDidLayoutSubviews 的实现和上面一样。

我不认为这些问题可以通过任何方式完全解决。当使用带有滚动视图的自动布局时,我一直有这种烦人的感觉,那就是我正在编写糟糕的代码。所以我欢迎进一步的见解。我也很好奇 iOS 7 是否会影响这些问题。

【讨论】:

以上是关于关闭模式视图控制器后,框架不反映自动布局约束的主要内容,如果未能解决你的问题,请参考以下文章

以编程方式从超级视图中删除视图后更新约束/框架(自动布局)

自动布局约束错误

iOS - 具有自动布局的依赖约束控制台输出

自动布局约束没有得到尊重

使用自动布局为动画准备视图

CGRectOffset 后自动布局不起作用