1

I have a custom UIView using UIKit Dynamics to perform an animation when the user taps on a button. The view in question is a simple one, that I lay out manually in layoutSubviews(). However, layoutSubviews() gets called for each frame of animation while UIKit Dynamics are in action, and any layout changes I make in that time (responding, for instance, to a taller status bar) result in distortion of my dynamic views.

How can I respond to a change in view size while a UIKit Dynamics animation is in progress?

Update

I created a demo project (which very closely matches my use case, though it's stripped down), and posted it on GitHub. The storyboard uses AutoLayout, but the view opts out of AutoLayout for laying out its own subviews with translatesAutoresizingMaskIntoConstraints = false. To reproduce the behavior, run in the simulator (I chose iPhone 5) and then hit ⌘Y as the star swings to witness the distortion. This is the view code:

import UIKit

class CustomView: UIView {

    var swingingView: UIView!

    var animator: UIDynamicAnimator!
    var attachment: UIAttachmentBehavior!

    var lastViewFrame = CGRectZero


    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)

        self.translatesAutoresizingMaskIntoConstraints = false

        swingingView = UIImageView(image: UIImage(named: "Star"))
        self.addSubview(swingingView)
    }

    override func layoutSubviews() {
        // Don't run for every frame of the animation. Only when responding to a layout change
        guard self.frame != lastViewFrame else {
            return
        }

        lastViewFrame = self.frame

        swingingView.frame = CGRect(x: 0, y: self.frame.size.height / 2, width: 100, height: 100)

        // Only run this setup code once
        if animator == nil {
            animator = UIDynamicAnimator(referenceView: self)

            let gravity = UIGravityBehavior(items: [swingingView])
            gravity.magnitude = 1.5
            animator.addBehavior(gravity)

            attachment = UIAttachmentBehavior(item: swingingView,
                offsetFromCenter: UIOffset(horizontal: 0, vertical: swingingView.frame.size.height / -2),
                attachedToAnchor: CGPoint(x: self.bounds.size.width / 2, y: 0))
            attachment.length = CGFloat(250.0)
            animator.addBehavior(attachment)
        }

        animator.updateItemUsingCurrentState(swingingView)
    }
}
Dov
  • 15,530
  • 13
  • 76
  • 177
  • have you tried creating a custom instance of `UIViewController` instead and putting your animations and layout inside of `viewDidLoad()` ? That would solve the problem of your layout code being triggered multiple times. – NSGangster Nov 19 '15 at 22:35
  • Can you put the code here? I think what may be happening is your autoLayout is being based off your animated view so while it animates, updateConstraints is being called and it distorts your views based off the animation. If this is the case make sure your other views constraints are based off layout guide margins and not the view being animated – NSGangster Nov 19 '15 at 22:48
  • Well I guess the best thing to do is debug if you are using variables to set your frames. Maybe set breakpoints at where your frames are being set in `layoutSubviews()` and see what values are changing. I can't be much help without the code I'm afraid. – NSGangster Nov 19 '15 at 23:12
  • @MatthewLawrenceBailey I added a code sample to the question, and a full project to GitHub: https://github.com/abbeycode/LayoutDuringDynamics – Dov Nov 20 '15 at 18:30

2 Answers2

3

You should use func updateItemUsingCurrentState(_ item: UIDynamicItem) per the UIDynamicAnimator class reference

A dynamic animator automatically reads the initial state (position and rotation) of each dynamic item you add to it, and then takes responsibility for updating the item’s state. If you actively change the state of a dynamic item after you’ve added it to a dynamic animator, call this method to ask the animator to read and incorporate the new state.

beyowulf
  • 15,101
  • 2
  • 34
  • 40
  • That's a great tip, but if the layout changes during a rotation animation, my view still gets warped. I even tried setting the view's `transform` to identity, but that didn't work either. With this approach though, the view only gets distorted once, though, and stays that way. Before, it would morph throughout the animation. I'll try to get a sample project together. – Dov Nov 20 '15 at 16:08
  • I added a code sample to the question, and a full project to GitHub: https://github.com/abbeycode/LayoutDuringDynamics – Dov Nov 20 '15 at 18:29
1

The issue is that you are re-setting the frame of a view that has been rotated (i.e. that has a transform applied to it). The frame is the size of the view within the parent's context. and thus you are unintentionally changing the bounds of this image view.

This issue is compounded by the fact that you're using the default content mode of .ScaleToFill and thus when the bounds of the image view change, the star is getting (further) distorted. (Note, the image wasn't square to start with, so I'd personally use .ScaleAspectFit, but that's up to you.)

Anyway, you should be able to remedy this problem by (a) setting the frame when you first add the UIImageView to the view hierarchy; (b) do not change the frame in layoutSubviews, but rather just adjust the center of the image view.

Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • Thanks, but adjusting the frame was intentional. In my actual app, I want the image to actually scale down a little when the view shrinks. Is there no way to achieve that? – Dov Nov 20 '15 at 21:41
  • Sure, just change the `bounds`. The `bounds` is within the coordinate system of the subview. The `frame` is within the coordinate system of the superview (after transform is applied). – Rob Nov 20 '15 at 21:46
  • So I should change the `bounds` and the `center` and that won't have the same effect as changing `frame`? I'll give it a shot. – Dov Nov 20 '15 at 21:47
  • Correct, setting `bounds` and `center` is quite different than changing the `frame`. If you change the `frame`, the behavior is now a function of the amount of rotation that happens to be applied to the `transform` of the view (which is why you see distortion and the behavior altering based upon how much the view was rotated when you reset the `frame`). I guess, if you really wanted, you could temporarily reset the `transform` back to identity, and then setting the `frame` probably would work, but I'm not sure why you'd do that. – Rob Nov 20 '15 at 21:51
  • @Rob I ask a related question here (https://stackoverflow.com/questions/45339297/uikit-dynamics-change-a-view-frame-with-collisions). Would be much obliged if you could take a look. – user1272965 Jul 27 '17 at 00:12