ChatGPT解决这个技术问题 Extra ChatGPT

如何为约束更改设置动画?

我正在使用 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 talk Best Practices for Mastering Auto Layout。它讨论了如何使用 CoreAnimation 更新约束:

https://i.stack.imgur.com/576mp.png

我尝试使用以下代码,但得到完全相同的结果:

- (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;
}

附带说明一下,我已经检查了很多次,这是在主线程上执行的。

我以前从来没有见过这么多投票给一个关于 SO 上的错字的问题和答案
如果答案中有错字,您应该编辑答案。这就是为什么它们是可编辑的。
@jeffamaphone - 如果您指出错字会更有用,这样我就知道错误在哪里。您可以自己编辑答案并修复拼写错误,从而节省其他所有人的诽谤。我只是编辑它以从动画块中删除常量,如果这就是您所指的。
我不知道错字是什么。我是在回复上面的评论。
那么错字就是问题所在。愚蠢的是,我输入的是“setNeedsLayout”而不是“layoutIfNeeded”。当我剪切并粘贴带有错误的代码和带有正确命令的屏幕截图时,它在我的问题中清楚地显示出来。然而在有人指出之前似乎无法注意到它。

i
iwasrobbed

两个重要说明:

您需要在动画块中调用 layoutIfNeeded。 Apple 实际上建议您在动画块之前调用一次,以确保所有待处理的布局操作都已完成您需要专门在父视图(例如 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()
}

你知道吗......你的答案有效。 WWDC 工作......我的愿景失败了。出于某种原因,我花了一周的时间才意识到我调用的是 setNeedsLayout 而不是 layoutIfNeeded。我花了多少小时没有注意到我只是输入了错误的方法名称,这让我有点害怕。
该解决方案有效,但您无需更改动画块内的约束常量。在开始动画之前设置一次约束是完全可以的。你应该编辑你的答案。
这最初对我不起作用,然后我意识到您需要在父视图上调用 layoutIfNeeded,而不是约束适用的视图。
使用 layoutIfNeeded 将为所有子视图刷新设置动画,而不仅仅是约束更改。您如何仅对约束更改进行动画处理?
“苹果其实建议你在动画块之前调用一次,以确保所有待处理的布局操作都已经完成”,谢谢,没想过,但有道理。
M
Mikolaj

我很欣赏提供的答案,但我认为更进一步会很好。

文档中的基本块动画

[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
}];

但实际上这是一个非常简单的场景。如果我想通过 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];
}

The AutoLayout Guide 有很多不足之处,但值得一读。我自己将其用作 UISwitch 的一部分,该 UISwitch 使用一对带有简单而微妙的折叠动画(0.2 秒长)的 UITextField 切换子视图。如上所述,子视图的约束在 UIView 子类 updateConstraints 方法中处理。


self.view(而不是该视图的子视图)上调用上述所有方法时,不需要调用 updateConstraintsIfNeeded(因为 setNeedsLayout 也会触发该视图的 updateConstraints)。对大多数人来说可能是微不足道的,但直到现在才适合我;)
在您的特定情况下,如果不了解 Autolayout 和弹簧和支柱布局的完整相互作用,就很难发表评论。我完全在谈论一个纯粹的自动布局方案。我很想知道您的 layoutSubviews 方法中到底发生了什么。
translatesAutoresizingMaskIntoConstraints = NO;如果你想要纯自动布局,你绝对应该禁用它。
我还没有看到,但是如果没有一些代码,我真的无法谈论这个问题。也许您可以模拟一个 TableView 并将其粘贴在 GitHub 上?
注意:如果使用 UIViewAnimationOptionBeginFromCurrentState,布局约束将在动画之前设置!
S
Steven Hepting

通常,您只需要更新约束并在动画块内调用 layoutIfNeeded。这可以是更改 NSLayoutConstraint.constant 属性、添加删除约束 (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];
}];

示例设置:

https://i.stack.imgur.com/EuPOs.jpg

争议

关于是否应该在动画块之前或内部更改约束存在一些问题(请参阅之前的答案)。

以下是教 iOS 的 Martin Pilkington 和编写 Auto Layout 的 Ken Ferry 之间的 Twitter 对话。 Ken 解释说,虽然在动画块之外更改常量目前可能 起作用,但这并不安全,它们确实应该在在动画块内部 进行更改。 https://twitter.com/kongtomorrow/status/440627401018466305

动画:

https://i.stack.imgur.com/xakEC.gif

示例项目

这是一个简单的项目,展示了如何为视图设置动画。它使用 Objective C 并通过更改多个约束的 .active 属性来为视图设置动画。 https://github.com/shepting/SampleAutoLayoutAnimation


+1 用于显示使用新活动标志的示例。此外,更改动画块之外的约束对我来说总是感觉像是一种黑客行为
我在 github 中提出了一个问题,因为此解决方案不适用于将视图移回原来的位置。我认为更改活动不是正确的解决方案,您应该更改优先级。将顶部设置为 750,将底部设置为 250,然后在代码中交替使用 UILayoutPriorityDefaultHigh 和 UILayoutPriorityDefaultLow。
在块内更新的另一个原因是,它将更改与调用代码隔离开来,调用代码可能碰巧也调用 layoutIfNeeded。 (感谢 Twitter 链接)
很好的答案。特别是因为你提到了马丁和肯关于这件事的谈话。
我对更新约束的方式感到困惑并写了 this。你能看看吗?
J
John Erck
// 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];
}];

M
Milan Nosáľ

斯威夫特 4 解决方案

UIView.animate

三个简单的步骤:

更改约束,例如: heightAnchor.constant = 50 告诉包含视图它的布局是脏的,并且自动布局应该重新计算布局: self.view.setNeedsLayout() 在动画块中告诉布局重新计算布局,相当于直接设置框架(在这种情况下,自动布局将设置框架): 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 是对动画的封装,我们可以一直引用它,以后再调用它。然而,由于在动画块中我们只是告诉自动布局重新计算帧,我们必须在调用 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()

更改约束和启动 animator 的顺序很重要 - 如果我们只是更改约束并将 animator 留到稍后的某个时间点,下一个重绘周期可以调用自动布局重新计算,并且更改不会被动画化。

另外,请记住,单个动画师是不可重复使用的——一旦你运行它,你就不能“重新运行”它。所以我想没有一个很好的理由让动画师留在身边,除非我们用它来控制交互式动画。


layoutIfNeeded() 是关键
K
Karen Hovhannisyan

快速解决方案:

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

A
Adam Preble

故事板、代码、提示和一些陷阱

其他答案都很好,但这个答案使用最近的一个例子突出了一些相当重要的动画约束的陷阱。在我意识到以下几点之前,我经历了很多变化:

将您想要定位的约束设置为类变量以保持强引用。在 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 的过滤器方法来分离将用作拐点的所需约束。一个也可能变得更复杂,但过滤器在这里做得很好。

使用 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

        }
    }
}

许多错误的转弯

这些笔记实际上是我为自己写的一组技巧。我个人和痛苦地做了所有不该做的事情。希望本指南可以避免其他人。

注意 zPositioning。有时当什么都没有发生时,您应该隐藏一些其他视图或使用视图调试器来定位您的动画视图。我什至发现了用户定义的运行时属性在故事板的 xml 中丢失并导致动画视图被覆盖(在工作时)的情况。总是花一点时间阅读文档(新旧)、快速帮助和标题。 Apple 不断进行大量更改以更好地管理 AutoLayout 约束(请参阅堆栈视图)。或者至少是 AutoLayout Cookbook。请记住,有时最好的解决方案是在较旧的文档/视频中。使用动画中的值并考虑使用其他 animateWithDuration 变体。不要将特定布局值硬编码为确定其他常量更改的标准,而是使用允许您确定视图位置的值。 CGRectContainsRect 就是一个例子 如果需要,不要犹豫,使用与参与约束定义的视图关联的布局边距 let viewMargins = self.webview.layoutMarginsGuide: is on example 不要做你不必做的工作,在情节提要上有约束的所有视图都有附加到属性 self.viewName.constraints 的约束 将任何约束的优先级更改为小于 1000。我在情节提要上将我的设置为 250(低)或 750(高); (如果您尝试将 1000 优先级更改为代码中的任何内容,则应用程序将崩溃,因为需要 1000)考虑不要立即尝试使用 activateConstraints 和 deactivateConstraints (它们有它们的位置,但在刚刚学习时,或者如果您正在使用故事板使用这些可能意味着你做的太多了~它们确实有一个位置,如下所示)考虑不要使用 addConstraints / removeConstraints 除非你真的在代码中添加了一个新的约束。我发现大多数时候我在情节提要中使用所需的约束来布局视图(将视图放置在屏幕外),然后在代码中,我为先前在情节提要中创建的约束设置动画以移动视图。我花了很多时间来建立新的 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,很可能什么都不会发生:在这种情况下,您可能有一个半生不熟的解决方案,如下所示:

注意 - 花点时间阅读下面的 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 Guide 中的片段(注意第二个片段用于使用 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];

对于那些在视觉上学习得更好的人,请尽早查看此video from Apple

密切关注

通常在文档中会有一些小注释或代码片段会导致更大的想法。例如,将自动布局约束附加到动态动画师是一个好主意。

祝你好运,愿原力与你同在。


C
C0mrade

工作解决方案 100% Swift 5.3

我已经阅读了所有答案,并希望分享我在所有应用程序中使用的代码和行的层次结构以正确设置它们的动画,这里的一些解决方案不起作用,你应该在较慢的设备上检查它们,例如 iPhone 5。

self.btnHeightConstraint.constant = 110
UIView.animate(withDuration: 0.27) { [weak self] in 
     self?.view.layoutIfNeeded()
}

G
Gabriel.Massana

我试图为约束设置动画,但很难找到一个好的解释。

其他答案所说的完全正确:您需要在 animateWithDuration: animations: 内调用 [self.view layoutIfNeeded];。但是,另一个重要的一点是为您要设置动画的每个 NSLayoutConstraint 设置指针。

I created an example in GitHub


E
Edoardo Vicoli

使用 Xcode 8.3.3 为 Swift 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) 的约束。我在 self.view 而不是 self.calendarView 上调用了 .layoutIfNeeded()

希望这有帮助。


D
D.D.

有一篇文章谈论这个: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];
        }];
    }
}

希望能帮助到你。


i
ivanferdelja

在约束动画的上下文中,我想提一下我在keyboard_opened 通知中立即对约束进行动画处理的特定情况。

约束定义了从文本字段到容器顶部的顶部空间。打开键盘后,我只需将常数除以 2。

我无法直接在键盘通知中实现一致的平滑约束动画。大约一半的时间视图只会跳转到新位置 - 没有动画。

我突然想到,由于键盘打开,可能会发生一些额外的布局。添加一个具有 10 毫秒延迟的简单 dispatch_after 块使动画每次都运行 - 没有跳跃。