Как использовать автоматический макет с контейнерными переходами?
Как вы можете использовать автоматический макет с помощью метода перехода контейнера 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, вы устанавливаете начальные кадры (непосредственно перед вызовом этого метода) и настраиваете финальные кадры в блоке анимации, которые вы передаете методу.
Этот метод выполняет работу по добавлению представления в иерархию представлений и выполнению анимаций для вас.
Проблема заключается в том, что мы не можем добавлять начальные ограничения в одно и то же место (до вызова метода), потому что представление еще не добавлено в иерархию представления.
Любые идеи, как я могу использовать этот метод вместе с Auto Layout?
Ниже приведен пример (Спасибо, кокоэнетика) за это, используя Springs/Struts (frames)
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];
}];
}
Ответы
Ответ 1
Начинаем думать об утилите
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];
}];
}
Ответ 2
Реальное решение, похоже, заключается в настройке ваших ограничений в блоке анимации 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];
}];
Ответ 3
Существует два решения в зависимости от того, нужно ли просто позиционировать представление с помощью автоматической компоновки (легко) или необходимости анимировать изменения ограничения автоматической компоновки (более сложные).
TL; версия DR
Если вам нужно только позиционировать представление с помощью автоматического макета, вы можете использовать метод -[UIViewController transitionFromViewController:toViewController:duration:options:animations:completion:]
и установить ограничения в блоке анимации.
Если вам нужно анимировать изменения ограничений автоматического макета, вы должны использовать общий вызов +[UIView animateWithDuration:delay:options:animations:completion:]
и регулярно добавлять дочерний контроллер.
Решение 1: Поместите вид через автоматический макет
Сначала разрешите первый, простой случай. В этом случае представление должно быть расположено с помощью автоматического макета, так что изменения высоты строки состояния (например, путем выбора Переключить строку состояния вызова), помимо прочего, не будут вытеснять вещи с экрана.
Для справки: официальный код Apple относительно перехода от одного контроллера к другому:
- (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). Это означает, что мы можем либо установить ограничения сразу после вызова переходаFromViewController, либо мы можем сделать это как первую строку в блоке анимации. Оба должны работать. Если вы хотите сделать это в кратчайшие сроки, то положить его в блок анимации - это путь. Более подробно о порядке вызова этих блоков будет рассказано ниже.
В целом, для простого позиционирования с помощью автоматического макета используйте шаблон, например:
- (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...
.
Для справки, здесь является стандартным способом добавления/удаления контроллера детского представления:
- (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
}
Сравнивая эти два метода и оригинальный циклFromViewController: выше, мы видим, что переходFromViewController выполняет две вещи для нас:
-
[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);
});
}
Теперь ясно, почему невозможно использовать переход для перехода к изменениям ограничений. В первый раз вы можете инициализировать ограничения после добавления вида (строка A). Ограничения должны быть анимированы в блоке animations()
(строка B), но не существует способа запуска кода между этими двумя строками.
Поэтому мы должны использовать блок ручной анимации вместе со стандартным методом анимации ограничений :
- (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
встроенного представления через раскадровку и метод выше, он будет сообщать YES
для раскадровки и NO
(очевидно, так как мы явно устанавливаем его в NO) для метод, который я рекомендую выше.
Это может привести к несоответствиям в вашем приложении, так как некоторые части системы, похоже, зависят от UIViewController, которые будут использоваться с translatesAutoresizingMaskIntoConstraints
, установленными на NO
. Например, на iPad Air (8.4), вы можете получить странное поведение при повороте с портрета на пейзаж.
Простым решением является сохранение translatesAutoresizingMaskIntoConstraints
в NO
, затем установка newViewController.view.frame = newViewController.view.superview.bounds
. Однако, если вы не очень осторожны при вызове этого метода, скорее всего, это даст вам неправильный визуальный макет. (Примечание: способ, которым раскадровка обеспечивает правильность размеров представления, заключается в установке свойства встроенного вида autoresize
на W+H
. Распечатка кадра сразу после добавления поднабора также выявит разницу между раскадровкой и программным подходом, который предполагает, что Apple устанавливает фрейм непосредственно на скрытый вид.)
Ответ 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 "Page на основе приложения", и это выполняется в проекте с поддержкой автоматического макета.
Итак, если бы я был вами, я бы попробовал установить кадры для анимации перехода контроллера представления и посмотреть, что произойдет. Дайте мне знать, как это работает.