I am trying to seamlessly transition a view from one view controller and have it "jump" into a sheet.
Apple does this in their widget picker for example when you add one of the top widgets.
Based on all the documentation I've read, the most common approach to this is to snapshot the area/view that you are trying to seamlessly transition in the presented view controller (the place where the item is coming from. And then have that snapshot translate across into where it should be in the layout of the presenting view controller.
func makeAnimatorIfNeeded(
using transitionContext: UIViewControllerContextTransitioning
) -> UIViewPropertyAnimator {
if let animator = animator {
return animator
}
// Presentation animation
let animator = UIViewPropertyAnimator(
duration: 0.5,
timingParameters: UISpringTimingParameters(dampingRatio: 1.0)
)
guard let fromVC = transitionContext.viewController(forKey: .from),
let toVC = transitionContext.viewController(forKey: .to)
else {
transitionContext.completeTransition(false)
return animator
}
/// !IMPORTANT: For some weird reason, accessing the views of the view controller
/// directly, like `fromVC.view` or `toVC.view` will break the sheet transition and gestures.
/// Instead, use the `view(forKey:)` method of the `transitionContext` to get the views.
let fromView = transitionContext.view(forKey: .from)
let toView = transitionContext.view(forKey: .to)
let containerView = transitionContext.containerView
let fromFrame = transitionContext.viewController(forKey: .from).map { transitionContext.finalFrame(for: $0) } ?? containerView.bounds
let toFrame = transitionContext.viewController(forKey: .to).map { transitionContext.finalFrame(for: $0) } ?? containerView.bounds
// Calculate the frame of sourceView relative to fromVC.view to capture the correct snapshot area.
let snapshotFrameInFromVC = sourceView?.convert(sourceView?.frame ?? .zero, to: fromVC.view) ?? containerView.frame
// Calculate the frame of sourceView relative to containerView to position the snapshot correctly.
let snapshotFrameInContainer = sourceView?.convert(sourceView?.frame ?? .zero, to: containerView) ?? containerView.frame
let snapshot: UIView?
if isPresenting {
// Create a snapshot of fromVC.view from the defined area (snapshotFrameInFromVC).
let originalColor = fromVC.view.backgroundColor
fromVC.view.backgroundColor = .clear
snapshot = fromVC.view.resizableSnapshotView(from: snapshotFrameInFromVC,
afterScreenUpdates: true,
withCapInsets: .zero)
fromVC.view.backgroundColor = originalColor
// Position the snapshot correctly within the snapshot container
snapshot?.frame = snapshotFrameInContainer
toView?.frame = toFrame
toView?.transform = CGAffineTransform(translationX: 0, y: containerView.frame.size.height)
toView?.layoutIfNeeded()
if let fromView {
containerView.addSubview(fromView)
}
if let toView {
containerView.addSubview(toView)
containerView.addSubview(snapshot ?? UIView())
}
let toViewCenter = CGPoint(
x: toVC.view.bounds.midX,
y: toVC.view.bounds.midY + 55
)
let gestureVelocity = CGPoint(x: 0, y: -5000)
animator.addAnimations {
Wave.animate(
withSpring: self.animatedSpring,
mode: .animated,
gestureVelocity: gestureVelocity
) {
snapshot?.animator.frame.size = CGSize(width: 204, height: 204) // Important to animate first
snapshot?.animator.center = toViewCenter
} completion: { finished, retargeted in
print("finished: \(finished), retargeted: \(retargeted)")
}
toView?.transform = CGAffineTransform.identity
}
animator.addCompletion { animatingPosition in
switch animatingPosition {
case .end:
snapshot?.removeFromSuperview()
transitionContext.completeTransition(true)
default:
transitionContext.completeTransition(false)
}
}
} else {
// Transitioning view is fromView
if let toView {
containerView.addSubview(toView)
}
if let fromView {
containerView.addSubview(fromView)
}
animator.addAnimations {
fromView?.transform = CGAffineTransform(translationX: 0, y: containerView.frame.size.height)
}
animator.addCompletion { animatingPosition in
switch animatingPosition {
case .end:
transitionContext.completeTransition(true)
default:
transitionContext.completeTransition(false)
}
}
}
self.animator = animator
return animator
}
I can pull this off seamlessly if the animation involves a simple movement from A to B.
But if I try to resize the snapshot as well, the snapshot becomes pixelated and low quality (I assume this is because the snapshot is literally a small screenshot and I'm stretching it from 56x56 to 204x204).
Is there another way that I'm overlooking to pull this off without a snapshot? Or is there a way I can resize the snapshot without losing quality?
Here is the animator code, it works great without the sizing so this should help future people looking to replicate the same effect anyways.