如何使用容器过渡的自动布局?

时间:2014-01-02 19:26:00

标签: ios uiviewcontroller autolayout

如何使用UIViewController容器转换方法使用自动布局:

-(void)transitionFromViewController:(UIViewController *)fromViewController
                   toViewController:(UIViewController *)toViewController 
                           duration:(NSTimeInterval)duration
                            options:(UIViewAnimationOptions)options
                         animations:(void (^)(void))animations
                         completion:(void (^)(BOOL finished))completion;

传统上,使用Springs / Struts,您可以设置初始帧(在调用此方法之前),并在传递给方法的动画块中设置最终帧。

该方法可以将视图添加到视图层次结构并为您运行动画。

问题在于,我们无法在同一位置添加初始约束(在方法调用之前),因为视图尚未添加到视图层次结构中。

如何将此方法与自动布局一起使用?

以下是使用Springs / Struts(框架)执行此操作的示例(感谢cocoanetics) http://www.cocoanetics.com/2012/04/containing-viewcontrollers

- (void)transitionFromViewController:(UIViewController *)fromViewController toViewController:(UIViewController *)toViewController
{

    // XXX We can't add constraints here because the view is not yet in the view hierarchy
    // animation setup 
    toViewController.view.frame = _containerView.bounds;
    toViewController.view.autoresizingMask = _containerView.autoresizingMask;

    // notify
    [fromViewController willMoveToParentViewController:nil];
    [self addChildViewController:toViewController];

    // transition
    [self transitionFromViewController:fromViewController
                      toViewController:toViewController
                              duration:1.0
                               options:UIViewAnimationOptionTransitionCurlDown
                            animations:^{
                            }
                            completion:^(BOOL finished) {
                                [toViewController didMoveToParentViewController:self];
                                [fromViewController removeFromParentViewController];
                            }];
}

4 个答案:

答案 0 :(得分:11)

开始考虑实用方法 transitionFromViewController:toViewController:duration:options:animations:无法使用Auto Layout干净地完成工作。

现在我已经用直接调用每个“低级”包含方法替换了我对此方法的使用。这是一个更多的代码,但似乎提供更大的控制。

看起来像这样:

- (void) performTransitionFromViewController:(UIViewController*)fromVc toViewController:(UIViewController*)toVc {

    [fromVc willMoveToParentViewController:nil];
    [self addChildViewController:toVc];

    UIView *toView = toVc.view;
    UIView *fromView = fromVc.view;

    [self.containerView addSubview:toView];

    // TODO: set initial layout constraints here

    [self.containerView layoutIfNeeded];

    [UIView animateWithDuration:.25
                          delay:0
                        options:0
                     animations:^{

                         // TODO: set final layout constraints here

                         [self.containerView layoutIfNeeded];

                     } completion:^(BOOL finished) {
                         [toVc didMoveToParentViewController:self];
                         [fromView removeFromSuperview];
                         [fromVc removeFromParentViewController];
                     }];
}

答案 1 :(得分:8)

真正的解决方案似乎是在transitionFromViewController:toViewController:duration:options:animations:的动画块中设置约束。

[self transitionFromViewController:fromViewController
                  toViewController:toViewController
                          duration:1.0
                           options:UIViewAnimationOptionTransitionCurlDown
                        animations:^{
                            // SET UP CONSTRAINTS HERE
                        }
                        completion:^(BOOL finished) {
                            [toViewController didMoveToParentViewController:self];
                            [fromViewController removeFromParentViewController];
                        }];

答案 2 :(得分:8)

根据您是否只需要通过自动布局(简单)定位视图与需要动画自动布局约束更改(更难),有两种解决方案。

TL; DR版本

如果您只需要通过自动布局定位视图,则可以使用-[UIViewController transitionFromViewController:toViewController:duration:options:animations:completion:]方法并在动画块中安装约束。

如果需要为自动布局约束更改设置动画,则必须使用通用+[UIView animateWithDuration:delay:options:animations:completion:]调用并定期添加子控制器。

解决方案1:通过自动布局

定位视图

让我们先解决第一个简单案例。在这种情况下,视图应通过自动布局定位,以便更改状态栏高度(例如,通过选择切换呼叫状态栏)等等,不会将事情从屏幕上移开

作为参考,这里有Apple's official code关于从一个视图控制器到另一个视图控制器的转换:

- (void) cycleFromViewController: (UIViewController*) oldC
            toViewController: (UIViewController*) newC
{
    [oldC willMoveToParentViewController:nil];                        // 1
    [self addChildViewController:newC];

    newC.view.frame = [self newViewStartFrame];                       // 2
    CGRect endFrame = [self oldViewEndFrame];

    [self transitionFromViewController: oldC toViewController: newC   // 3
          duration: 0.25 options:0
          animations:^{
             newC.view.frame = oldC.view.frame;                       // 4
             oldC.view.frame = endFrame;
           }
           completion:^(BOOL finished) {
             [oldC removeFromParentViewController];                   // 5
             [newC didMoveToParentViewController:self];
            }];
}

我们必须添加约束,而不是像上例中那样使用框架。问题是在哪里添加它们。我们无法在上面的标记(2)处添加它们,因为newC.view未安装在视图层次结构中。它只在我们调用transitionFromViewController...(3)时安装。这意味着我们可以在调用transitionFromViewController之后立即安装约束,或者我们可以将其作为动画块中的第一行。两者都应该有效。如果你想在最早的时候做,那么把它放在动画块中就是要走的路。下面将讨论如何调用这些块的顺序。

总之,只需通过自动布局进行定位,请使用以下模板:

- (void)cycleFromViewController:(UIViewController *)oldViewController
               toViewController:(UIViewController *)newViewController
{
    [oldViewController willMoveToParentViewController:nil];
    [self addChildViewController:newViewController];

    newViewController.view.alpha = 0;

    [self transitionFromViewController:oldViewController
                      toViewController:newViewController
                              duration:0.25
                               options:0
                            animations:^{
                                newViewController.view.translatesAutoresizingMaskIntoConstraints = NO;
                                // create constraints for newViewController.view here

                                newViewController.view.alpha = 1;
                            }
                            completion:^(BOOL finished) {
                                [oldViewController removeFromParentViewController];
                                [newViewController didMoveToParentViewController:self];
                            }];
    // or create constraints right here
}

解决方案2:动画约束更改

动画约束更改并不那么简单,因为在视图附加到层次结构和通过transitionFromViewController...方法调用动画块之间时,我们没有给出回调。

作为参考,here是添加/删除子视图控制器的标准方法:

- (void) displayContentController: (UIViewController*) content;
{
   [self addChildViewController:content];                 // 1
   content.view.frame = [self frameForContentController]; // 2
   [self.view addSubview:self.currentClientView];
   [content didMoveToParentViewController:self];          // 3
}

- (void) hideContentController: (UIViewController*) content
{
   [content willMoveToParentViewController:nil];  // 1
   [content.view removeFromSuperview];            // 2
   [content removeFromParentViewController];      // 3
}

通过比较这两个方法和上面发布的原始cycleFromViewController:我们看到transitionFromViewController为我们处理了两件事:

  • [self.view addSubview:self.currentClientView];
  • [content.view removeFromSuperview];

通过添加一些日志记录(在这篇文章中省略),我们可以很好地了解何时调用这些方法。

执行此操作后,似乎该方法以类似的方式实现,如下所示:

- (void)transitionFromViewController:(UIViewController *)fromViewController toViewController:(UIViewController *)toViewController duration:(NSTimeInterval)duration options:(UIViewAnimationOptions)options animations:(void (^)(void))animations completion:(void (^)(BOOL finished))completion
{
    [self.view addSubview:toViewController.view];  // A
    animations();                                  // B

    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(duration * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        [fromViewController.view removeFromSuperview];
        completion(YES);
    });
}

现在可以清楚地看到为什么不能使用transitionFromViewController来为约束更改设置动画。第一次初始化约束是在添加视图之后(第A行)。约束应该在animations()块(B行)中设置动画,但是无法在这两行之间运行代码。

因此,我们必须使用手动动画块以及standard method of animating constraint changes

- (void)cycleFromViewController:(UIViewController *)oldViewController
               toViewController:(UIViewController *)newViewController
{
    [oldViewController willMoveToParentViewController:nil];
    [self addChildViewController:newViewController];

    [self.view addSubview:newViewController.view];
    newViewController.view.translatesAutoresizingMaskIntoConstraints = NO;
    // TODO: create initial constraints for newViewController.view here

    [newViewController.view layoutIfNeeded];

    // TODO: update constraint constants here

    [UIView animateWithDuration:0.25
                     animations:^{
                         [newViewController.view layoutIfNeeded];
                     }
                     completion:^(BOOL finished) {
                         [oldViewController.view removeFromSuperview];
                         [oldViewController removeFromParentViewController];
                         [newViewController didMoveToParentViewController:self];
                     }];
}

警告

这不等于故事板嵌入容器视图控制器的方式。例如,如果您通过故事板与上述方法比较嵌入视图的translatesAutoresizingMaskIntoConstraints值,则会报告故事板的YESNO(显然,因为我们已明确对于我上面推荐的方法,将其设置为NO。

这可能会导致您的应用程序出现不一致,因为系统的某些部分似乎依赖于UIViewController包含来与translatesAutoresizingMaskIntoConstraints设置为NO一起使用。例如,on an iPad Air (8.4), you may get strange behavior when rotating from portrait to landscape

简单的解决方案似乎是将translatesAutoresizingMaskIntoConstraints设置为NO,然后设置newViewController.view.frame = newViewController.view.superview.bounds。但是,除非您在调用此方法时非常小心,否则很可能会给您一个不正确的视觉布局。 (注意:故事板确保视图大小正确的方式是将嵌入视图的autoresize属性设置为W+H。在添加子视图后立即打印出框架也会显示出差异在故事板与编程方法之间,这表明Apple正在直接在所包含的视图上设置框架。)

答案 3 :(得分:4)

我希望你的问题有所吸引力,因为我认为这是一个很好的问题。我没有给你一个确定的答案,但我可以描述自己与类似的情况的经验。

以下是我从经验中得出的结论:您不能直接在视图控制器的根视图上使用自动布局。一旦我在根视图上将translatesAutoresizingMaskIntoConstraints设置为NO,我就会开始出错或更糟。

所以我使用混合解决方案。我设置框架并使用自动调整来在布局中定位和调整根视图的大小,否则由自动布局配置。例如,以下是我在使用自动布局的应用程序中viewDidLoad中将页面视图控制器加载为子视图控制器的方法:

self.pageViewController = ...  
...

[self addChildViewController:self.pageViewController];
[self.view addSubview:self.pageViewController.view];

// could not get constraints to work here (using autoresizing mask)
self.pageViewController.view.frame = self.view.bounds;

[self.pageViewController didMoveToParentViewController:self];

这是Apple在Xcode“基于页面的应用程序”模板中加载子视图控制器的方式 - 这是在启用自动布局的项目中执行的。

所以,如果我是你,我会尝试设置帧来动画视图控制器转换,看看会发生什么。让我知道它是如何工作的。