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

Я обновляю старое приложение с помощью 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;
}

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

ОБНОВЛЕНИЕ: я повторно смотрел доклад WWDC 12 "Лучшие практики для освоения автоматической компоновки", который охватывает анимацию. Здесь обсуждается, как обновить ограничения с помощью 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;
}

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

Ответ 1

Две важные заметки:

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

  2. Вы должны вызвать его специально для родительского представления (например, self.view), а не для дочернего представления, к которому привязаны ограничения. Это приведет к обновлению всех ограниченных представлений, включая анимирование других представлений, которые могут быть ограничены представлением, в котором вы изменили ограничение (например, View B прикреплен к нижней части View 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;
}

Swift 3

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

Ответ 2

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

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

[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?

Блок анимации, вызывающий метод subviews 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 и должен вызывать супер в конце метода.

- (void)updateConstraints
{
    // Update some constraints

    [super updateConstraints];
}

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

Ответ 3

Как правило, вам просто нужно обновить ограничения и вызвать 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 Project, так что пример анимационного проекта.

Полемика

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

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

Анимация:

xakEC.gif

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

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

Ответ 4

// 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 parent
    [self.view layoutIfNeeded];
}];

Ответ 5

Решение Swift 4

UIView.animate

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

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

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

    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() чтобы убедиться, что начальная точка для анимации принадлежит состоянию со старыми ограничениями (в случае 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 представляет собой инкапсуляцию анимации, мы можем продолжать ссылаться на нее и называть ее позже. Однако, поскольку в блоке анимации мы просто говорим автозапуск, чтобы пересчитать кадры, мы должны изменить ограничения перед вызовом 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()

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

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

Ответ 6

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

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

Сделайте ограничения, на которые вы хотите нацелиться, в переменные класса, чтобы иметь сильную ссылку. В 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!
}()

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

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

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

Анимирование ограничений с помощью 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. Иногда, когда ничего не происходит, вы должны скрыть некоторые из других представлений или использовать отладчик вида для поиска вашего анимированного представления. Я даже обнаружил случаи, когда атрибут Runtime User Defined Runtime был потерян в xboard раскадровки и привел к тому, что анимированное представление было охвачено (во время работы).

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

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

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

  5. Если необходимо, не стесняйтесь использовать поля макета, связанные с представлением, участвующим в определении ограничения, let viewMargins = self.webview.layoutMarginsGuide: на примере
  6. Не выполняйте работу, которую вам не нужно делать, все представления с ограничениями на раскадровке имеют ограничения, связанные с собственностью self.viewName.constraints
  7. Измените приоритеты для любых ограничений до менее 1000. Я установил для меня 250 (низкий) или 750 (высокий) на раскадровке; (если вы попытаетесь изменить приоритет 1000 на что-либо в коде, тогда приложение выйдет из строя, потому что требуется 1000)
  8. Подумайте о том, чтобы не сразу пытаться использовать activateConstraints и deactivateConstraints (у них есть свое место, но когда вы просто учитесь или используете раскадровку, используя их, вероятно, это означает, что вы слишком много делаете ~ у них есть место, как показано ниже)
  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). Кстати, это уже не в текущем руководстве, насколько я вижу. Предпочтительные методы продолжают развиваться.

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

Если вам необходим полный контроль над анимацией изменений, сделанных с помощью автоматического макета, вы должны программно изменить свои ограничения. Основная концепция для 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.

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

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

Удачи и дайте Силе быть с вами.

Ответ 7

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

yourConstraint.constant = 50
UIView.animate(withDuration: 1.0, animations: {
    yourView.layoutIfNeeded
})

Ответ 8

Рабочее решение 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.
}

Ответ 9

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

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

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

Ответ 10

Рабочее и просто протестированное решение для 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 и NOT на self.calendarView

Надеюсь на эту помощь.

Ответ 11

Есть статья об этом: 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];
        }];
    }
}

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

Ответ 12

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

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

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

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

Ответ 13

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

UIView.Animate(5, () =>
{
    _addBannerDistanceFromBottomConstraint.Constant = 0;
    View.LayoutIfNeeded();
});