I have a dynamic setup of buttons that automatically adjust in width and height based on auto layout constraints set up in a storyboard. When in portrait the buttons have equal widths and heights so the frame is perfectly square, and when the device is rotated to landscape the buttons get shorter and wider. I have set cornerRadius
on the buttons' layers so they're perfectly circular when in portrait and that works well, but when I rotate to landscape the corner radius doesn't look right anymore obviously. I need to change that so it becomes an oval. The problem is, no matter where I try to put that code, I cannot get the correct width and height the buttons will be after the rotation occurs. I want this to occur while the device is being rotated - don't want to wait until the rotation has completed. Ideally the cornerRadius
would animate the change while the transition animates.
If I use viewWillLayoutSubviews
or viewDidLayoutSubviews
, it correctly gets the button frame when the app is launched, but upon rotating to landscape this method is called before the button's frame is updated for the new orientation, so I can't calculate the correct corner radius.
If I use viewWillTransitionToSize:withTransitionCoordinator:
or willTransitionToTraitCollection:withTransitionCoordinator:
or willRotateToInterfaceOrientation
, it doesn't get called when the app is launched, and upon rotating the device it is called before the frame is updated for the new size.
If I use willAnimateRotationToInterfaceOrientation
, it doesn't get called when the app is launched, but upon rotating the device it does correctly get the new button frame. But this method is deprecated.
So the question is, what method can you use to set the properties of a button based on the size the button will be when rotation has completed, that is called before the rotation has completed?
Note that it needs to be called for every orientation change, not just size class changes (rotating iPad doesn't change the size class). I only need to support iOS 8.0+.
This is the code I'm placing in the methods to know if it is getting the correct size:
println("\(button.frame.size.width) x \(button.frame.size.height)")
Below I outline how one could use a CADisplayLink
to update the properties during every frame of the animation, but there is an easier way. Subclass the button (or view or whatever) with a layoutSubviews
that updates the corner radius:
class RoundedCornerView: UIView {
override func layoutSubviews() {
super.layoutSubviews()
layer.cornerRadius = min(bounds.width, bounds.height) / 2
}
}
Then, during the animation, as the frame size changes, the corner radius is updated automatically:
As Joey points out, one can use viewWillTransitionToSize
to be notified of the new size, and then use animateAlongsideTransition
to coordinate your additional animation along with the main animation.
If you want to animate an un-animatable property, such as corner radius (in block-based animations, anyway), you can use a display link for the duration of the animation. This effectively calls our method for every frame of the rotation animation. thus, we'll have a method that looks at the presentation layer (which shows you the current state of a layer mid-animation) to adjust the corner radius on the fly. You can use animateAlongsideTransition
for this, too, but we're not using the animation
closure in this case, but rather merely using the completion
closure to know when the animation is done and therefore can stop the display link:
override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransitionToSize(size, withTransitionCoordinator: coordinator)
let displayLink = CADisplayLink(target: self, selector: "handleDisplayLink:")
displayLink.addToRunLoop(NSRunLoop.mainRunLoop(), forMode: NSRunLoopCommonModes)
coordinator.animateAlongsideTransition(nil) { (context) -> Void in
displayLink.invalidate()
}
}
func handleDisplayLink(displayLink: CADisplayLink) {
updateButtonsCornerRadius()
}
func updateButtonsCornerRadius() {
for button in [button1, button2] {
let presentationLayer = button.layer.presentationLayer() as CALayer
let minDimension = min(presentationLayer.frame.size.width, presentationLayer.frame.size.height)
button.layer.cornerRadius = minDimension / 2.0
}
}
Frankly, I'm not sure this corner radius animation is enough to justify this display link (I had to slow down animations in the simulator with command+T to appreciate the difference), but this illustrates the basic idea.
The answer is to use viewWillTransitionToSize:withTransitionCoordinator:
, with your code placed inside the animation closure of UIViewControllerTransitionCoordinator
's animateAlongsideTransition:
method. This will allow you to animate the changes based on the size it will be when rotation completes.
override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransitionToSize(size, withTransitionCoordinator: coordinator)
coordinator.animateAlongsideTransition({ (context: UIViewControllerTransitionCoordinatorContext!) -> Void in
self.updateButtonsCornerRadius()
}, completion: nil)
}
This method is not called when the app launches though so you'll want to call self.updateButtonsCornerRadius()
in viewDidLoad
as well.