Why?

Sooner or later an iOS developer needs custom animations between views. This is often a mixture of position changes and scaling mainly just a view’s frame to another view’s frame animation. You might first try something like this:

UIView.animate(withDuration: 1) { myView.frame = CGRect(x: 60, y: 60, width: 100, height: 100) } 1 2 3 UIView . animate ( withDuration : 1 ) { myView . frame = CGRect ( x : 60 , y : 60 , width : 100 , height : 100 ) }





Sometimes this will work, but not if myView has any subviews. For example, if myView has a label inside, then the frame of the superview will change but the label will stay as is. Instead, we need the label to scale along with the superview. We could apply the frame animation to the label’s frame, but while this changes the size of the label, the font doesn’t scale. We need a better approach. I’ll show you how to use the view’s transform property and layer’s position to achieve the goal.

Position and anchor point

In iOS each UIView has a layer with two interesting properties, for our purposes: anchorPoint and position . These two properties are connected and changing an anchorPoint or the position of the layer will change its placement in the parent view.

The position property is the position of the anchorPoint in the superview, so changing the anchorPoint or the position will affect the placement of the child view in its container superview. The position value represents points on screen, so setting the x = 1 will move the view by one point to the right. The anchorPoint value is different. For anchorPoint , setting x = 1 will set the anchorPoint at the right edge of the view, while x = 0 will set it to the left edge.

Let’s get to animations

Let’s clarify all this with some animations. Start with a view and subview:

let bigSquare = UIView(frame: CGRect(x: 0, y: 0, width: 120, height: 120)) bigSquare.backgroundColor = UIColor.green let smallSquare = UIView(frame: CGRect(x: 0, y: 0, width: 40, height: 40)) bigSquare.addSubview(smallSquare) smallSquare.backgroundColor = UIColor.blue 1 2 3 4 5 6 let bigSquare = UIView ( frame : CGRect ( x : 0 , y : 0 , width : 120 , height : 120 ) ) bigSquare . backgroundColor = UIColor . green let smallSquare = UIView ( frame : CGRect ( x : 0 , y : 0 , width : 40 , height : 40 ) ) bigSquare . addSubview ( smallSquare ) smallSquare . backgroundColor = UIColor . blue

Let’s have the initial square animate its frame from

smallSquare.frame = CGRect(x: 0, y: 0, width: 40, height: 40) 1 smallSquare . frame = CGRect ( x : 0 , y : 0 , width : 40 , height : 40 )

to

smallSquare.frame = CGRect(x: 20, y: 20, width: 80, height: 80) 1 smallSquare . frame = CGRect ( x : 20 , y : 20 , width : 80 , height : 80 )

Let’s break this down and start with just the position, so that the smallSquare ends up centered inside the container.

UIView.animate(withDuration: 1) { smallSquare.layer.position = CGPoint(x: 60, y: 60) } 1 2 3 UIView . animate ( withDuration : 1 ) { smallSquare . layer . position = CGPoint ( x : 60 , y : 60 ) }

Ok that was easy. Let’s now scale the smallSquare to twice its size while it’s moving to the center.

UIView.animate(withDuration: 1) { smallSquare.layer.position = CGPoint(x: 60, y: 60) smallSquare.transform = CGAffineTransform.identity.scaledBy(x: 2, y: 2) } 1 2 3 4 UIView . animate ( withDuration : 1 ) { smallSquare . layer . position = CGPoint ( x : 60 , y : 60 ) smallSquare . transform = CGAffineTransform . identity . scaledBy ( x : 2 , y : 2 ) }

Great, it looks good. We can now use this as a base for creating a new function that will move the view from its initial frame to a new one.

func animateTo(frame: CGRect, withDuration duration: TimeInterval, completion: ((Bool) -> Void)? = nil) { guard let _ = superview else { return } let xScale = frame.size.width / self.frame.size.width let yScale = frame.size.height / self.frame.size.height let x = frame.origin.x + (self.frame.width * xScale)/2 let y = frame.origin.y + (self.frame.height * yScale)/2 UIView.animate(withDuration: duration, delay: 0, options: .curveLinear, animations: { self.layer.position = CGPoint(x: x, y: y) self.transform = self.transform.scaledBy(x: xScale, y: yScale) }, completion: completion) 1 2 3 4 5 6 7 8 9 10 11 12 13 func animateTo ( frame : CGRect , withDuration duration : TimeInterval , completion : ( ( Bool ) -> Void ) ? = nil ) { guard let _ = superview else { return } let xScale = frame . size . width / self . frame . size . width let yScale = frame . size . height / self . frame . size . height let x = frame . origin . x + ( self . frame . width * xScale ) / 2 let y = frame . origin . y + ( self . frame . height * yScale ) / 2 UIView . animate ( withDuration : duration , delay : 0 , options : . curveLinear , animations : { self . layer . position = CGPoint ( x : x , y : y ) self . transform = self . transform . scaledBy ( x : xScale , y : yScale ) } , completion : completion )

Now we can write our above transitions as

smallSquare.animateTo(frame: CGRect(x: 60, y: 60, width: 80, height: 80), withDuration: 1) 1 smallSquare . animateTo ( frame : CGRect ( x : 60 , y : 60 , width : 80 , height : 80 ) , withDuration : 1 )

Now for the anchorPoint

We’ve assumed that the anchorPoint is always set to x: 0.5 and y: 0.5. This is the default position, but someone could have moved the anchorPoint to a different position and we need to takes this into account. We need to change our function from

let x = frame.origin.x + (self.frame.width * xScale)/2 let y = frame.origin.y + (self.frame.height * yScale)/2 1 2 let x = frame . origin . x + ( self . frame . width * xScale ) / 2 let y = frame . origin . y + ( self . frame . height * yScale ) / 2

to

let x = frame.origin.x + (self.frame.width * xScale) * self.layer.anchorPoint.x let y = frame.origin.y + (self.frame.height * yScale) * self.layer.anchorPoint.y 1 2 let x = frame . origin . x + ( self . frame . width * xScale ) * self . layer . anchorPoint . x let y = frame . origin . y + ( self . frame . height * yScale ) * self . layer . anchorPoint . y

Now regardless of the anchorPoint’s position, the views will move and scale correctly. For the purpose of scaling and position changes we don’t need to move the anchorPoint of a layer, we can simply rely on layer.position and transform to achieve the effect of animation originating from any point in the view.

Summary

The method I created may make your life a little easier and your app a little slicker. Below is the full function as an extension to UIView. I will also attach a playground file so you can experiment.