Use UIKit Dynamics with Auto Layout

Use UIKit Dynamics with Auto Layout

Here we are ...

Here at arconsis, we pride ourselves in solving technological, conceptual and user experience related challenges on a high level. That involves continuous learning and sometimes doing things that haven’t been done before. Since we’re also strong believers when it comes to sharing knowledge, we’re going to blog about some of those topics and learnings here, starting with this post.

Let's go

As an iOS developer, animations are fun. Even more fun is UIKit Dynamics.

However, what has always bothered me so far is the fact that Dynamics is not compatible with Auto Layout. So how can you solve this circumstance?

How do I do UIView animations?

To do this, I look at how I dealt with normal UIView animations and brought my Auto Layout User interfaces to life.
Simple example: I have a UILabel that is layouted from either the interface builder or code. To keep the layout constraints
while animating a nice zoom effect, I just use the transform matrix.

That would look something like this:

private func animateZoomAndRotation() {
    UIView.animate(withDuration: 0.1, animations: {
        self.label.transform = CGAffineTransform(scaleX: 2, y: 2).concatenating(CGAffineTransform(rotationAngle: -0.5))
    }) { finished in
        self.animateBounceToStartPosition()
    }
}

private func animateBounceToStartPosition() {
    UIView.animate(withDuration: 0.4, delay: 0.0, usingSpringWithDamping: 0.4, initialSpringVelocity: 0.7, options: .allowUserInteraction, animations: {
        self.label.transform = .identity
    }, completion: nil)
}

Do you remember the school lesson, where transform matrices where introduced? No? No problem - you can read up the basics on Wikipedia. But luckily we don't have to know how transform matrices work in detail.
Apple provided us some helper functions to create translations, scales, and rotations.
I use the .concatenating() function to chain both scaling and a slight rotation. Afterwards, I assign the .identity matrix again, say: go back to the original state. If I wrap the whole thing in a Spring animation, I get a nice bounce effect in addition.

With this understanding, one can also approach the Dynamics principle. We want Dynamics to only adjust the transform matrix.

Use a proxy!

To be eligible to participate in UIKit Dynamics a UIView implements the UIDynamicItem protocol. Dynamics then modifies the center, bounds and transform properties.
We need a little trick, so we can get around the fact, that Dynamics tries to do more stuff with our view than altering the transform matrix.
To achieve this we create an object that also implements the UIDynamicItem protocol. This object will be used by Dynamics to apply all transformations. That way, Dynamics does not influence any property of our view directly.

How should we call a thing that mediates between two systems? Exactly: a proxy.

And this is how it looks:

class DynamicProxyItem: NSObject, UIDynamicItem {

    var center: CGPoint
    var bounds: CGRect
    var transform: CGAffineTransform

    private let originalCenter: CGPoint
    private let originalBounds: CGRect
    private let originalTransform: CGAffineTransform

    var absoluteTransform: CGAffineTransform {
        let translationVector = CGPoint(x: center.x - originalCenter.x, y: center.y - originalCenter.y)
        let translation = CGAffineTransform(translationX: translationVector.x, y: translationVector.y)
        return originalTransform.concatenating(transform).concatenating(translation)
    }

    init(view: UIView) {
        center = view.center
        bounds = view.bounds
        transform = view.transform

        originalCenter = center
        originalBounds = bounds
        originalTransform = transform
        super.init()
    }
}

We need an NSObject here because UIDynamicItem inherits from the NSObjectProtocol. You initialize the proxy item with a given view. This is the view we actually want to be animated.

The absoluteTransform computed property is used to get the current position based on its original transform matrix. Because the modified transform matrix does not have any information about translations, we have to compute the translation manually by calculating the translation vector and concatenating the translation matrix.

Voilá! We have finished our Dynamic proxy item!

Now, everywhere where we use UIKit Dynamics and any UIDynamicBehavior object we only have to implement the behaviors action handler. This handler is always called when the behavior changes its corresponding dynamic item. So this is the place where we have to pick the absoluteTransform from the proxy and assign it to our view.

private func configureBehaviorActions() {
    let behavior = UIItemBehavior()

    // ... set up item behavior

    behavior.action = {
        self.applyProxyTransformToRealView()
    }        
}

private func applyProxyTransformToAnimatedView() {
    self.animatedView.transform = self.proxyView.absoluteTransform
}

That's all.

Head over to Github and download the complete example project. (When you build and run - use a device - Core Motion is used within this demo)

What do think? Do you have questions? Just shoot me a message on Twitter or simply leave me an email (orlando.schaefer@arconsis.com)

Keep dynamic. Have fun!