Как мне анимировать изменения ограничений?

Я обновляю старое приложение с AdBannerView и когда нет рекламы, она соскальзывает с экрана. Когда есть реклама, она скользит по экрану. Основные вещи.

По старому стилю я установил кадр в анимационном блоке. Новый стиль, у меня есть IBOutlet в ограничение, которое определяет позицию Y, в этом случае это расстояние от нижней части суперпредставления, и измените константу.

- (void)moveBannerOffScreen {
    [UIView animateWithDuration:5
             animations:^{
                          _addBannerDistanceFromBottomConstraint.constant = -32;
                     }];
    bannerIsVisible = FALSE;
}

- (void)moveBannerOnScreen {
    [UIView animateWithDuration:5
             animations:^{
                         _addBannerDistanceFromBottomConstraint.constant = 0;
             }];
    bannerIsVisible = TRUE;
}

И баннер движется именно так, как и ожидалось, но без анимации.

ОБНОВЛЕНИЕ: я повторно посмотрел видео WWDC12 " Best Practices для освоения автоматической компоновки", которое охватывает анимацию. Здесь обсуждается, как обновить ограничения с помощью CoreAnimation,

Я пытался с помощью следующего кода, но получить точно такие же результаты.

- (void)moveBannerOffScreen {
    _addBannerDistanceFromBottomConstraint.constant = -32;
    [UIView animateWithDuration:2
                     animations:^{
                         [self.view setNeedsLayout];
                     }];
    bannerIsVisible = FALSE;
}

- (void)moveBannerOnScreen {
    _addBannerDistanceFromBottomConstraint.constant = 0;
    [UIView animateWithDuration:2
                     animations:^{
                         [self.view setNeedsLayout];
                     }];
    bannerIsVisible = TRUE;
}

Кроме того, я проверял множество раз, и это выполняется в главном потоке.

13 ответов

Решение

Два важных замечания:

  1. Вам нужно позвонить layoutIfNeeded в блоке анимации. Apple фактически рекомендует вызывать его один раз перед блоком анимации, чтобы убедиться, что все ожидающие операции макета были завершены

  2. Вы должны вызывать его конкретно в родительском представлении (например, self.view), а не дочернее представление, к которому прикреплены ограничения. Это обновит все ограниченные представления, включая анимацию других представлений, которые могут быть ограничены видом, для которого вы изменили ограничение (например, представление B прикреплено к нижней части представления A, и вы только что изменили верхнее смещение представления A, и вам нужно представление B). оживить с этим)

Попробуй это:

Objective-C

- (void)moveBannerOffScreen {
    [self.view layoutIfNeeded];

    [UIView animateWithDuration:5
        animations:^{
            self._addBannerDistanceFromBottomConstraint.constant = -32;
            [self.view layoutIfNeeded]; // Called on parent view
        }];
    bannerIsVisible = FALSE;
}

- (void)moveBannerOnScreen { 
    [self.view layoutIfNeeded];

    [UIView animateWithDuration:5
        animations:^{
            self._addBannerDistanceFromBottomConstraint.constant = 0;
            [self.view layoutIfNeeded]; // Called on parent view
        }];
    bannerIsVisible = TRUE;
}

Свифт 3

UIView.animate(withDuration: 5) {
    self._addBannerDistanceFromBottomConstraint.constant = 0
    self.view.layoutIfNeeded()
}

Я ценю предоставленный ответ, но я думаю, что было бы неплохо пойти еще дальше.

Основной блок анимации из документации

[containerView layoutIfNeeded]; // Ensures that all pending layout operations have been completed
[UIView animateWithDuration:1.0 animations:^{
     // Make all constraint changes here
     [containerView layoutIfNeeded]; // Forces the layout of the subtree animation block and then captures all of the frame changes
}];

но на самом деле это очень упрощенный сценарий. Что делать, если я хочу анимировать ограничения subview через updateConstraints метод?

Блок анимации, который вызывает метод updateConstraints для подпредставлений

[self.view layoutIfNeeded];
[self.subView setNeedsUpdateConstraints];
[self.subView updateConstraintsIfNeeded];
[UIView animateWithDuration:1.0f delay:0.0f options:UIViewAnimationOptionLayoutSubviews animations:^{
    [self.view layoutIfNeeded];
} completion:nil];

Метод updateConstraints переопределяется в подклассе UIView и должен вызывать super в конце метода.

- (void)updateConstraints
{
    // Update some constraints

    [super updateConstraints];
}

Руководство по AutoLayout оставляет желать лучшего, но его стоит прочитать. Я сам использую это как часть UISwitch что переключает подпредставление с парой UITextFieldс простой и тонкой анимацией коллапса (0,2 секунды). Ограничения для подпредставления обрабатываются в методах updateConstraints подклассов UIView, как описано выше.

Как правило, вам просто нужно обновить ограничения и вызвать layoutIfNeeded внутри блока анимации. Это может быть либо изменение .constant свойство NSLayoutConstraintдобавление удаления ограничений (iOS 7) или изменение .active свойство ограничений (iOS 8 и 9).

Образец кода:

[UIView animateWithDuration:0.3 animations:^{
    // Move to right
    self.leadingConstraint.active = false;
    self.trailingConstraint.active = true;

    // Move to bottom
    self.topConstraint.active = false;
    self.bottomConstraint.active = true;

    // Make the animation happen
    [self.view setNeedsLayout];
    [self.view layoutIfNeeded];
}];

Пример настройки:

Проект XCode, так пример проекта анимации

полемика

Есть несколько вопросов о том, следует ли изменить ограничение перед блоком анимации или внутри него (см. Предыдущие ответы).

Ниже приводится беседа в Твиттере между Мартином Пилкингтоном, который преподает iOS, и Кеном Ферри, который написал Auto Layout. Кен объясняет, что хотя изменение констант за пределами блока анимации может в настоящее время работать, это небезопасно, и их действительно следует изменять внутри блока анимации. https://twitter.com/kongtomorrow/status/440627401018466305

Анимация:

Пример проекта

Вот простой проект, показывающий, как можно анимировать вид. Он использует Objective C и оживляет вид, изменяя .active свойство нескольких ограничений. https://github.com/shepting/SampleAutoLayoutAnimation

// Step 1, update your constraint
self.myOutletToConstraint.constant = 50; // New height (for example)

// Step 2, trigger animation
[UIView animateWithDuration:2.0 animations:^{

    // Step 3, call layoutIfNeeded on your animated view's parent
    [self.view layoutIfNeeded];
}];

Swift 4 решение

UIView.animate

Три простых шага:

  1. Измените ограничения, например:

    heightAnchor.constant = 50
    
  2. Скажи содержание view что его макет грязный и что autoLayout должен пересчитать макет:

    self.view.setNeedsLayout()
    
  3. В анимационном блоке укажите макету пересчитать макет, что эквивалентно установке кадров непосредственно (в этом случае автопоставка установит кадры):

    UIView.animate(withDuration: 0.5) {
        self.view.layoutIfNeeded()
    }
    

Полный простейший пример:

heightAnchor.constant = 50
self.view.setNeedsLayout()
UIView.animate(withDuration: 0.5) {
    self.view.layoutIfNeeded()
}

Примечание

Существует необязательный 0-й шаг - перед изменением ограничений вы можете захотеть вызвать self.view.layoutIfNeeded() чтобы убедиться, что отправная точка для анимации находится в состоянии со примененными старыми ограничениями (в случае, если произошли некоторые другие изменения ограничений, которые не должны быть включены в анимацию):

otherConstraint.constant = 30
// this will make sure that otherConstraint won't be animated but will take effect immediately
self.view.layoutIfNeeded()

heightAnchor.constant = 50
self.view.setNeedsLayout()
UIView.animate(withDuration: 0.5) {
    self.view.layoutIfNeeded()
}

UIViewPropertyAnimator

Так как с iOS 10 мы получили новый механизм анимации - UIViewPropertyAnimator, мы должны знать, что в основном к нему применяется тот же механизм. Шаги в основном одинаковы:

heightAnchor.constant = 50
self.view.setNeedsLayout()
let animator = UIViewPropertyAnimator(duration: 0.5, timingParameters: UICubicTimingParameters(animationCurve: .linear))
animator.addAnimations {
    self.view.layoutIfNeeded()
}
animator.startAnimation()

поскольку animator это инкапсуляция анимации, мы можем сохранить ссылку на нее и вызвать ее позже. Тем не менее, поскольку в блоке анимации мы просто сообщаем autolayout пересчитать кадры, мы должны изменить ограничения перед вызовом startAnimation, Поэтому что-то подобное возможно:

// prepare the animator first and keep a reference to it
let animator = UIViewPropertyAnimator(duration: 0.5, timingParameters: UICubicTimingParameters(animationCurve: .linear))
animator.addAnimations {
    self.view.layoutIfNeeded()
}

// at some other point in time we change the constraints and call the animator
heightAnchor.constant = 50
self.view.setNeedsLayout()
animator.startAnimation()

Порядок изменения ограничений и запуска аниматора важен - если мы просто изменим ограничения и оставим наш аниматор для более поздней точки, следующий цикл перерисовки может вызвать пересчет автоматического размещения, и изменение не будет анимированным.

Кроме того, помните, что один аниматор не предназначен для повторного использования - как только вы запустите его, вы не сможете его "перезапустить". Так что я думаю, что на самом деле нет веской причины держать аниматора рядом, если только мы не используем его для управления интерактивной анимацией.

Быстрое решение:

yourConstraint.constant = 50
UIView.animateWithDuration(1, animations: yourView.layoutIfNeeded)

Раскадровка, код, советы и несколько ошибок

Другие ответы просто хороши, но этот выдвигает на первый план несколько довольно важных моментов анимации ограничений. Я прошел через множество вариаций, прежде чем понял следующее:

Установите ограничения, на которые вы хотите нацелить переменные класса, чтобы они содержали сильную ссылку. В Swift я использовал ленивые переменные:

lazy var centerYInflection:NSLayoutConstraint = {
       let temp =  self.view.constraints.filter({ $0.firstItem is MNGStarRating }).filter ( { $0.secondItem is UIWebView }).filter({ $0.firstAttribute == .CenterY }).first
        return temp!
}()

После некоторых экспериментов я заметил, что один ДОЛЖЕН получить ограничение из представления НАД (выше супервизии) двух представлений, где ограничение определено. В приведенном ниже примере (и MNGStarRating, и UIWebView - это два типа элементов, между которыми я создаю ограничение, и они являются подпредставлениями в self.view).

Цепочка фильтров

Я использую метод фильтра Свифта, чтобы отделить желаемое ограничение, которое будет служить точкой перегиба. Можно также сделать намного более сложным, но фильтр делает хорошую работу здесь.

Анимация ограничений с помощью Swift

Nota Bene - Этот пример является решением раскадровки / кода и предполагает, что в раскадровке были установлены ограничения по умолчанию. Затем можно анимировать изменения с помощью кода.

Предполагая, что вы создаете свойство для фильтрации с точными критериями и получаете конкретную точку перегиба для вашей анимации (конечно, вы также можете фильтровать массив и проходить через него, если вам нужно несколько ограничений):

lazy var centerYInflection:NSLayoutConstraint = {
    let temp =  self.view.constraints.filter({ $0.firstItem is MNGStarRating }).filter ( { $0.secondItem is UIWebView }).filter({ $0.firstAttribute == .CenterY }).first
    return temp!
}()

....

Некоторое время спустя...

@IBAction func toggleRatingView (sender:AnyObject){

    let aPointAboveScene = -(max(UIScreen.mainScreen().bounds.width,UIScreen.mainScreen().bounds.height) * 2.0)

    self.view.layoutIfNeeded()


    //Use any animation you want, I like the bounce in springVelocity...
    UIView.animateWithDuration(1.0, delay: 0.0, usingSpringWithDamping: 0.3, initialSpringVelocity: 0.75, options: [.CurveEaseOut], animations: { () -> Void in

        //I use the frames to determine if the view is on-screen
        if CGRectContainsRect(self.view.frame, self.ratingView.frame) {

            //in frame ~ animate away
            //I play a sound to give the animation some life

            self.centerYInflection.constant = aPointAboveScene
            self.centerYInflection.priority = UILayoutPriority(950)

        } else {

            //I play a different sound just to keep the user engaged
            //out of frame ~ animate into scene
            self.centerYInflection.constant = 0
            self.centerYInflection.priority = UILayoutPriority(950)
            self.view.setNeedsLayout()
            self.view.layoutIfNeeded()
         }) { (success) -> Void in

            //do something else

        }
    }
}

Много неправильных поворотов

Эти заметки действительно набор советов, которые я написал для себя. Я сделал все, что не делал лично и мучительно. Надеюсь, это руководство может пощадить других.

  1. Остерегайтесь zPosition. Иногда, когда ничего не происходит, вам следует скрыть некоторые другие виды или использовать отладчик, чтобы найти анимированный вид. Я даже нашел случаи, когда определяемый пользователем атрибут времени выполнения терялся в xml раскадровки и приводил к покрытию анимированного представления (во время работы).

  2. Всегда уделяйте минуту чтению документации (новой и старой), быстрой справки и заголовков. Apple продолжает вносить множество изменений, чтобы лучше управлять ограничениями AutoLayout (см. Стековые представления). Или, по крайней мере, поваренная книга AutoLayout. Имейте в виду, что иногда лучшие решения находятся в старой документации / видео.

  3. Поиграйте со значениями в анимации и рассмотрите возможность использования других вариантов animateWithDuration.

  4. Не указывайте конкретные значения макета в качестве критериев для определения изменений других констант, вместо этого используйте значения, которые позволяют вам определить местоположение представления. CGRectContainsRect это один пример

  5. При необходимости не стесняйтесь использовать поля макета, связанные с представлением, участвующим в определении ограничения. let viewMargins = self.webview.layoutMarginsGuide: находится на примере
  6. Не выполняйте работу, которую не нужно делать, все представления с ограничениями на раскадровке имеют ограничения, связанные с свойством self.viewName.constraints
  7. Измените ваши приоритеты для любых ограничений на менее чем 1000. Я установил свой на 250 (низкий) или 750 (высокий) на раскадровке; (если вы попытаетесь изменить приоритет 1000 на что-либо в коде, приложение завершится сбоем, потому что требуется 1000)
  8. Не стоит сразу пытаться использовать activConstraints и de activateConstraints (они имеют свое место, но когда вы просто учитесь или если вы используете раскадровку, используя их, вероятно, это означает, что вы делаете слишком много - у них действительно есть место, хотя, как показано ниже)
  9. Не используйте addConstraints / removeConstraints, если вы действительно не добавляете новое ограничение в коде. Я обнаружил, что в большинстве случаев я размещаю представления в раскадровке с желаемыми ограничениями (помещая представление за пределы экрана), затем в коде я анимирую ограничения, ранее созданные в раскадровке, для перемещения представления.
  10. Я потратил много времени на создание ограничений с помощью нового класса и подклассов NSAnchorLayout. Они отлично работают, но мне потребовалось некоторое время, чтобы понять, что все ограничения, которые мне нужны, уже существуют в раскадровке. Если вы строите ограничения в коде, то наверняка используйте этот метод для объединения ваших ограничений:

Быстрый образец решений, чтобы избежать при использовании раскадровки

private var _nc:[NSLayoutConstraint] = []
    lazy var newConstraints:[NSLayoutConstraint] = {

        if !(self._nc.isEmpty) {
            return self._nc
        }

        let viewMargins = self.webview.layoutMarginsGuide
        let minimumScreenWidth = min(UIScreen.mainScreen().bounds.width,UIScreen.mainScreen().bounds.height)

        let centerY = self.ratingView.centerYAnchor.constraintEqualToAnchor(self.webview.centerYAnchor)
        centerY.constant = -1000.0
        centerY.priority = (950)
        let centerX =  self.ratingView.centerXAnchor.constraintEqualToAnchor(self.webview.centerXAnchor)
        centerX.priority = (950)

        if let buttonConstraints = self.originalRatingViewConstraints?.filter({

            ($0.firstItem is UIButton || $0.secondItem is UIButton )
        }) {
            self._nc.appendContentsOf(buttonConstraints)

        }

        self._nc.append( centerY)
        self._nc.append( centerX)

        self._nc.append (self.ratingView.leadingAnchor.constraintEqualToAnchor(viewMargins.leadingAnchor, constant: 10.0))
        self._nc.append (self.ratingView.trailingAnchor.constraintEqualToAnchor(viewMargins.trailingAnchor, constant: 10.0))
        self._nc.append (self.ratingView.widthAnchor.constraintEqualToConstant((minimumScreenWidth - 20.0)))
        self._nc.append (self.ratingView.heightAnchor.constraintEqualToConstant(200.0))

        return self._nc
    }()

Если вы забудете один из этих или более простых советов, например, о том, где добавить layoutIfNeeded, скорее всего, ничего не произойдет: в этом случае у вас может получиться полуобжаренное решение, подобное следующему

NB. Найдите минутку, чтобы прочитать раздел AutoLayout ниже и оригинальное руководство. Есть способ использовать эти методы в дополнение к вашим динамическим аниматорам.

UIView.animateWithDuration(1.0, delay: 0.0, usingSpringWithDamping: 0.3, initialSpringVelocity: 1.0, options: [.CurveEaseOut], animations: { () -> Void in

            //
            if self.starTopInflectionPoint.constant < 0  {
                //-3000
                //offscreen
                self.starTopInflectionPoint.constant = self.navigationController?.navigationBar.bounds.height ?? 0
                self.changeConstraintPriority([self.starTopInflectionPoint], value: UILayoutPriority(950), forView: self.ratingView)

            } else {

                self.starTopInflectionPoint.constant = -3000
                 self.changeConstraintPriority([self.starTopInflectionPoint], value: UILayoutPriority(950), forView: self.ratingView)
            }

        }) { (success) -> Void in

            //do something else
        }

    }

Фрагмент из руководства AutoLayout (обратите внимание, что второй фрагмент предназначен для использования OS X). Кстати - это больше не в текущем руководстве, насколько я вижу. Предпочтительные методы продолжают развиваться.

Анимация изменений, сделанных автоматическим макетом

Если вам нужен полный контроль над анимацией изменений, внесенных с помощью Auto Layout, вы должны внести свои ограничения программно. Основная концепция одинакова как для iOS, так и для OS X, но есть несколько незначительных отличий.

В приложении для iOS ваш код будет выглядеть примерно так:

[containerView layoutIfNeeded]; // Ensures that all pending layout operations have been completed
[UIView animateWithDuration:1.0 animations:^{
     // Make all constraint changes here
     [containerView layoutIfNeeded]; // Forces the layout of the subtree animation block and then captures all of the frame changes
}];

В OS X используйте следующий код при использовании анимаций на основе слоев:

[containterView layoutSubtreeIfNeeded];
[NSAnimationContext runAnimationGroup:^(NSAnimationContext *context) {
     [context setAllowsImplicitAnimation: YES];
     // Make all constraint changes here
     [containerView layoutSubtreeIfNeeded];
}];

Когда вы не используете анимации на основе слоев, вы должны анимировать константу с помощью аниматора ограничения:

[[constraint animator] setConstant:42];

Для тех, кто учится лучше визуально, посмотрите это раннее видео от Apple.

Обратить пристальное внимание

Часто в документации есть небольшие заметки или фрагменты кода, которые приводят к большим идеям. Например, присоединение ограничений автоматического размещения к динамическим аниматорам - большая идея.

Удачи и да пребудет с тобой Сила.

Рабочий раствор 100% Swift 3.1

Я прочитал все ответы и хочу поделиться кодом и иерархией строк, которые я использовал во всех своих приложениях для их правильной анимации. Некоторые решения здесь не работают, вы должны проверить их на более медленных устройствах, например, iPhone 5 в данный момент.

self.view.layoutIfNeeded() // Force lays of all subviews on root view
UIView.animate(withDuration: 0.5) { [weak self] in // allowing to ARC to deallocate it properly
       self?.tbConstraint.constant = 158 // my constraint constant change
       self?.view.layoutIfNeeded() // Force lays of all subviews on root view again.
}

Я пытался оживить ограничения, и мне было нелегко найти хорошее объяснение.

То, что говорят другие ответы, совершенно верно: вам нужно позвонить [self.view layoutIfNeeded]; внутри animateWithDuration: animations:, Тем не менее, другой важный момент заключается в том, чтобы указатели для каждого NSLayoutConstraint Вы хотите оживить.

Я создал пример в GitHub.

Рабочее и только что протестированное решение для Swift 3 с Xcode 8.3.3:

self.view.layoutIfNeeded()
self.calendarViewHeight.constant = 56.0

UIView.animate(withDuration: 0.5, delay: 0.0, options: UIViewAnimationOptions.curveEaseIn, animations: {
        self.view.layoutIfNeeded()
    }, completion: nil)

Просто помните, что self.calendarViewHeight является ограничением, относящимся к customView (CalendarView). Я вызвал.layoutIfNeeded() для self.view, а НЕ для self.calendarView

Надеюсь, это поможет.

Есть статья об этом: http://weblog.invasivecode.com/post/42362079291/auto-layout-and-core-animation-auto-layout-was

В котором он закодирован так:

- (void)handleTapFrom:(UIGestureRecognizer *)gesture {
    if (_isVisible) {
        _isVisible = NO;
        self.topConstraint.constant = -44.;    // 1
        [self.navbar setNeedsUpdateConstraints];  // 2
        [UIView animateWithDuration:.3 animations:^{
            [self.navbar layoutIfNeeded]; // 3
        }];
    } else {
        _isVisible = YES;
        self.topConstraint.constant = 0.;
        [self.navbar setNeedsUpdateConstraints];
        [UIView animateWithDuration:.3 animations:^{
            [self.navbar layoutIfNeeded];
        }];
    }
}

Надеюсь, поможет.

В контексте анимации ограничений я хотел бы упомянуть конкретную ситуацию, в которой я немедленно анимировал ограничение в уведомлении клавиатуры с открытием.

Ограничение определяет верхний пробел от текстового поля до вершины контейнера. После открытия клавиатуры я просто делю константу на 2.

Мне не удалось добиться согласованной плавной анимации ограничений непосредственно в уведомлении клавиатуры. Примерно в половине случаев изображение просто перейдет на новую позицию - без анимации.

Мне пришло в голову, что в результате открытия клавиатуры может возникнуть дополнительная раскладка. Добавление простого блока dispatch_after с задержкой в ​​10 мс заставляло анимацию запускаться каждый раз - без прыжков.

Для поддерживающих Xamarians, вот версия Xamarin.iOS/C#:

UIView.Animate(5, () =>
{
    _addBannerDistanceFromBottomConstraint.Constant = 0;
    View.LayoutIfNeeded();
});
Другие вопросы по тегам