Before explaining how to use PinLayout to animate views, you can check these two nice tutorials to learn the basic of animations:
It is important to remember that PinLayout is stateless, i.e. PinLayout always start from the view's current position and size (frame) when layouting a view. You don't need to reset anything to animate a view using PinLayout.
This also means that you can modify only the property you want to animate, for example the following code will only animate the view's width:
UIView.animate(withDuration: 0.3) {
view.pin.width(30)
}
Multiple strategies can be used to animate layout using PinLayout. The choice is a question of preferences and the kind of animations you want to achieve.
Some possible strategies will be shown below using a simple example. The example animates a view position from left to right when the user tap a button.
Note that in the following source code the view's size was set to 150 px (view.pin.size(150)
) in the initialization.
In this strategy, to force a call to layoutSubviews(), we call UIView.setNeedsLayout
and UIView.layoutIfNeeded
from the animation block.
var isViewLeftDocked = true
override func layoutSubviews() {
super.layoutSubviews()
if isViewLeftDocked {
view.pin.top().left()
} else {
view.pin.top().right()
}
}
func didTapTogglePosition() {
isViewLeftDocked = !isViewLeftDocked
UIView.animate(withDuration: 0.3) {
self.setNeedsLayout()
self.layoutIfNeeded()
}
}
This strategy use a private method to layout the animated view (layoutAnimatedView()
). The advantage of this solution is that it is not required to call UIView.setNeedsLayout
and UIView.layoutIfNeeded
to relayout the view.
var isViewLeftDocked = true
override func layoutSubviews() {
super.layoutSubviews()
layoutAnimatedView()
}
private func layoutAnimatedView() {
if isViewLeftDocked {
view.pin.top().left()
} else {
view.pin.top().right()
}
}
func didTapTogglePosition() {
isViewLeftDocked = !isViewLeftDocked
UIView.animate(withDuration: 0.3) {
self.layoutAnimatedView()
}
}
This strategy is similar to the previous one, but use an enumeration to keep the animation state.
enum AnimationState {
case leftDocked
case rightDocked
}
var animationState = AnimationState.leftDocked
override func layoutSubviews() {
super.layoutSubviews()
layoutAnimatedView()
}
private func layoutAnimatedView() {
switch animationState {
case .leftDocked:
view.pin.top().left()
case .rightDocked:
view.pin.top().right()
}
}
func didTapTogglePosition() {
switch animationState {
case .leftDocked: animationState = .rightDocked
case .rightDocked: animationState = .leftDocked
}
UIView.animate(withDuration: 0.3) {
self.layoutAnimatedView()
}
}
It's really up to you to think of animation's strategies that match your situation. With PinLayout you are always in control of everything, including animations.
In some particular situation it is possible that layoutSubViews()
may be called during the animation is in progress, this can occur particularly on long animation. To handle this kind of situation it is possible to use a boolean indicating if an animation is in progress, and to block temporarely the layout of animated views in layoutSubViews()
.
Here is an example:
enum AnimationState {
case leftDocked
case rightDocked
}
var animationState = AnimationState.leftDocked
var isAnimating = false
override func layoutSubviews() {
super.layoutSubviews()
// If an animation of the view is in progress, we don't update animated views position.
guard !isAnimating else { return }
layoutAnimatedView()
}
private func layoutAnimatedView() {
switch animationState {
case .leftDocked:
view.pin.top().left()
case .rightDocked:
view.pin.top().right()
}
}
func didTapTogglePosition() {
switch animationState {
case .leftDocked: animationState = .rightDocked
case .rightDocked: animationState = .leftDocked
}
UIView.animate(withDuration: 0.3, animations: {
self.isAnimating = true
self.layoutAnimatedView()
}, completion: { (_) in
self.isAnimating = false
})
}
You can check the animation example available in the PinLayout's Example App: