Create transition and interaction like iOS Photos app
I. class 구조 만들기
-
class ZoomTransitionController
UIViewControllerTransitioningDelegate
또는UINavigationControllerDelegate
를 구현하고 전환을 관리합니다.
-
class ZoomAnimator
UIViewControllerAnimatedTransitioning
및 줌 애니메이션 논리를 구현합니다.
-
ZoomAnimator는 ZoomTransitionController의 속성입니다.
-
두 클래스에서 ZoomAnimator 와 ZoomTransitionController 를 분리 한 이유 는 대화식 전환과 일반 전환을 관리하기 위해서입니다.
-
ZoomAnimator은 대화식 전환이 아닙니다.
II. 확대/축소 애니메이션 구현
1. ZoomAnimator
-
UIImageView를 가져 와서 확대 / 축소하고 전환 원본 프레임에서 전환 대상 프레임으로 애니메이션을 적용합니다.
// 델리게이트를 사용하여 전환 이미지와 프레임을 가져옵니다.
// 이 델리게이트는 전환소스/전환대상인 ViewController에 의해 구현됩니다
protocol ZoomAnimatorDelegate: class {func transitionWillStartWith(zoomAnimator: ZoomAnimator) func transitionDidEndWith(zoomAnimator: ZoomAnimator) func referenceImageView(for zoomAnimator: ZoomAnimator) -> UIImageView? func referenceImageViewFrameInTransitioningView(for zoomAnimator: ZoomAnimator) -> CGRect?
}
class ZoomAnimator: NSObject {
// 출발 weak var fromDelegate: ZoomAnimatorDelegate? // 도착 weak var toDelegate: ZoomAnimatorDelegate? // 애니메이션 이미지 var transitionImageView: UIImageView? // 목록에서 확대하여 세부 정보를 표시할지 세부 정보를 축소할지를 결정! var isPresenting: Bool = true
}
-
UIViewControllerAnimatedTransitioning
프로토콜의 두 메서드extension ZoomAnimator: UIViewControllerAnimatedTransitioning {
// 애니메이션의 지속 기간 func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { if self.isPresenting { return 0.5 } else { return 0.25 } } // 애니메이션이 실제로 실행되는 곳 // 이 메서드에서는 isPresenting 속성을 사용하여 확대 또는 축소를 확인합니다. func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { if self.isPresenting { animateZoomInTransition(using: transitionContext) } else { animateZoomOutTransition(using: transitionContext) } }
}
// 애니메이션의 지속 기간
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
if self.isPresenting {
return 0.5
} else {
return 0.25
}
}
// 애니메이션이 실제로 실행되는 곳
// 이 메서드에서는 isPresenting 속성을 사용하여 확대 또는 축소를 확인합니다.
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
if self.isPresenting {
animateZoomInTransition(using: transitionContext)
} else {
animateZoomOutTransition(using: transitionContext)
}
}
2. 애니메이션 로직
- 출발 및 도착 이미지 숨기기
- 출발 이미지에서 애니메이션을 만들 이미지 만들기
- 도착 이미지의 프레임 계산
- 출발 프레임에서 도착 프레임으로 이미지 애니메이션
확대
fileprivate func animateZoomInTransition(using transitionContext: UIViewControllerContextTransitioning) {
let containerView = transitionContext.containerView
guard let toVC = transitionContext.viewController(forKey: .to),
let fromVC = transitionContext.viewController(forKey: .from),
// 출발 imageView 가져오기
let fromReferenceImageView = self.fromDelegate?.referenceImageView(for: self),
// 도착 imageView 가져오기
let toReferenceImageView = self.toDelegate?.referenceImageView(for: self),
// 출발 imageView frame 가져오기
let fromReferenceImageViewFrame = self.fromDelegate?.referenceImageViewFrameInTransitioningView(for: self)
else {
return
}
self.fromDelegate?.transitionWillStartWith(zoomAnimator: self)
self.toDelegate?.transitionWillStartWith(zoomAnimator: self)
toVC.view.alpha = 0
// 도착 imageView 숨기기
toReferenceImageView.isHidden = true
containerView.addSubview(toVC.view)
let referenceImage = fromReferenceImageView.image!
// 확대/축소 애니메이션에 사용할 imageView 생성
if self.transitionImageView == nil {
let transitionImageView = UIImageView(image: referenceImage)
transitionImageView.contentMode = .scaleAspectFill
transitionImageView.clipsToBounds = true
transitionImageView.frame = fromReferenceImageViewFrame
self.transitionImageView = transitionImageView
containerView.addSubview(transitionImageView)
}
// 출발 imageView 숨기기
fromReferenceImageView.isHidden = true
// 애니메이션 이후 도착 imageView frame 계산하기
let finalTransitionSize = calculateZoomInImageFrame(image: referenceImage, forView: toVC.view)
UIView.animate(withDuration: transitionDuration(using: transitionContext),
delay: 0,
usingSpringWithDamping: 0.8,
initialSpringVelocity: 0,
options: [UIViewAnimationOptions.transitionCrossDissolve],
animations: {
// imageView의 frame 갱신
self.transitionImageView?.frame = finalTransitionSize
toVC.view.alpha = 1.0
fromVC.tabBarController?.tabBar.alpha = 0
},
completion: { completed in
// imageView 삭제
self.transitionImageView?.removeFromSuperview()
// 출발 imageView 및 도착 imageView 보이기
toReferenceImageView.isHidden = false
fromReferenceImageView.isHidden = false
self.transitionImageView = nil
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
self.toDelegate?.transitionDidEndWith(zoomAnimator: self)
self.fromDelegate?.transitionDidEndWith(zoomAnimator: self)
})
}
축소
fileprivate func animateZoomOutTransition(using transitionContext: UIViewControllerContextTransitioning) {
let containerView = transitionContext.containerView
guard let toVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to),
let fromVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from),
let fromReferenceImageView = self.fromDelegate?.referenceImageView(for: self),
let toReferenceImageView = self.toDelegate?.referenceImageView(for: self),
let fromReferenceImageViewFrame = self.fromDelegate?.referenceImageViewFrameInTransitioningView(for: self),
let toReferenceImageViewFrame = self.toDelegate?.referenceImageViewFrameInTransitioningView(for: self)
else {
return
}
self.fromDelegate?.transitionWillStartWith(zoomAnimator: self)
self.toDelegate?.transitionWillStartWith(zoomAnimator: self)
toReferenceImageView.isHidden = true
let referenceImage = fromReferenceImageView.image!
if self.transitionImageView == nil {
let transitionImageView = UIImageView(image: referenceImage)
transitionImageView.contentMode = .scaleAspectFill
transitionImageView.clipsToBounds = true
transitionImageView.frame = fromReferenceImageViewFrame
self.transitionImageView = transitionImageView
containerView.addSubview(transitionImageView)
}
containerView.insertSubview(toVC.view, belowSubview: fromVC.view)
fromReferenceImageView.isHidden = true
let finalTransitionSize = toReferenceImageViewFrame
UIView.animate(withDuration: transitionDuration(using: transitionContext),
delay: 0,
options: [],
animations: {
fromVC.view.alpha = 0
self.transitionImageView?.frame = finalTransitionSize
toVC.tabBarController?.tabBar.alpha = 1
}, completion: { completed in
self.transitionImageView?.removeFromSuperview()
toReferenceImageView.isHidden = false
fromReferenceImageView.isHidden = false
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
self.toDelegate?.transitionDidEndWith(zoomAnimator: self)
self.fromDelegate?.transitionDidEndWith(zoomAnimator: self)
})
}
3. ZoomTransitionController
-
ZoomTransitionController는 출발과 도착을 대리자로 참조하는 ZoomAnimator와 동일한 속성을 갖습니다.
class ZoomTransitionController: NSObject {
let animator: ZoomAnimator weak var fromDelegate: ZoomAnimatorDelegate? weak var toDelegate: ZoomAnimatorDelegate? ...
-
ZoomTransitionController는 변환의 시작 부분에 ZoomAnimator를 반환
extension ZoomTransitionController: UIViewControllerTransitioningDelegate {
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? { self.animator.isPresenting = true self.animator.fromDelegate = fromDelegate self.animator.toDelegate = toDelegate return self.animator } func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { self.animator.isPresenting = false let tmp = self.fromDelegate self.animator.fromDelegate = self.toDelegate self.animator.toDelegate = tmp return self.animator }
}
extension ZoomTransitionController: UINavigationControllerDelegate {
func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationControllerOperation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? { if operation == .push { self.animator.isPresenting = true self.animator.fromDelegate = fromDelegate self.animator.toDelegate = toDelegate } else { self.animator.isPresenting = false let tmp = self.fromDelegate self.animator.fromDelegate = self.toDelegate self.animator.toDelegate = tmp } return self.animator }
}
-
줌 애니메이션 설정이 완료되었습니다.
-
ViewController에서 ZoomTransitionController를 사용 하여 줌 전환을 만듭니다.
4. HomeContentViewController
-
VRDetailViewController 는 ZoomTransitionController 를 속성으로가집니다.
class VRDetailViewController: UIViewController, UIGestureRecognizerDelegate {
var transitionController = ZoomTransitionController() ...
}
-
CollectionView에서 셀을 탭하면 Segue를 실행하고 전환을 시작합니다.
prepare(for:sender:)
메서드에서 애니메이션 대리자를 설정합니다.override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "DetailSegue" { let nav = self.navigationController let vc = segue.destination as! VRDetailViewController // ZoomTransitionController에 navigationController 위임을 설정합니다. nav?.delegate = vc.transitionController vc.transitionController.fromDelegate = self vc.transitionController.toDelegate = vc ... }
}
-
이제 ZoomTransitionController 에 의한 전환은 UINavigationController 전환이 발생할 때 실행됩니다 .
-
나머지는 각 ViewController에 ZoomAnimatorDelegate 를 구현하고 ZoomAnimator 에 출발과 도착의 이미지와 프레임을 알려줌으로써 완성 됩니다.
extension HomeContentViewController: ZoomAnimatorDelegate {
func transitionWillStartWith(zoomAnimator: ZoomAnimator) { } func transitionDidEndWith(zoomAnimator: ZoomAnimator) { // CollectionView의 셀의 위치를 조정합니다 let cell = self.collectionView.cellForItem(at: self.selectedIndexPath) as! PhotoCollectionViewCell let cellFrame = self.collectionView.convert(cell.frame, to: self.view) if cellFrame.minY < self.collectionView.contentInset.top { self.collectionView.scrollToItem(at: self.selectedIndexPath, at: .top, animated: false) } else if cellFrame.maxY > self.view.frame.height - self.collectionView.contentInset.bottom { self.collectionView.scrollToItem(at: self.selectedIndexPath, at: .bottom, animated: false) } } func referenceImageView(for zoomAnimator: ZoomAnimator) -> UIImageView? { // CollectionView의 이미지를 반환합니다. let cell = self.collectionView.cellForItem(at: self.selectedIndexPath) as! PhotoCollectionViewCell return cell.imageView } func referenceImageViewFrameInTransitioningView(for zoomAnimator: ZoomAnimator) -> CGRect? { // CollectionView의 이미지의 프레임을 반환합니다. let cell = self.collectionView.cellForItem(at: self.selectedIndexPath) as! PhotoCollectionViewCell let cellFrame = self.collectionView.convert(cell.frame, to: self.view) if cellFrame.minY < self.collectionView.contentInset.top { return CGRect(x: cellFrame.minX, y: self.collectionView.contentInset.top, width: cellFrame.width, height: cellFrame.height - (self.collectionView.contentInset.top - cellFrame.minY)) } return cellFrame }
}
5. VRDetailViewController
extension VRDetailViewController: ZoomAnimatorDelegate {
func transitionWillStartWith(zoomAnimator: ZoomAnimator) {
}
func transitionDidEndWith(zoomAnimator: ZoomAnimator) {
}
func referenceImageView(for zoomAnimator: ZoomAnimator) -> UIImageView? {
return self.currentViewController.imageView
}
func referenceImageViewFrameInTransitioningView(for zoomAnimator: ZoomAnimator) -> CGRect? {
return self.currentViewController.scrollView.convert(self.currentViewController.imageView.frame, to: self.currentViewController.view)
}
}
- 확대/축소 애니메이션 완료!
III. 팬 제스처로 대화식(interactive) 전환 구현
1. interactive 전환 정의하기
- 확대 / 축소하지 않고 사진을 아래로 당기면 사진이 점차 작아집니다. 최소 크기가 존재합니다.
- 사진을 아래로 당길 때 사진의 손가락 위치가 나타납니다.
- 사진을 당기는 정도에 따라 배경이 투명 해집니다.
- 손가락을 위쪽으로 움직이면 사진이 원래 위치로 돌아갑니다.
2. ZoomDismissalInteractionController
-
ZoomDismissalInteractionController 는 UIViewControllerInteractiveTransitioning을 구현 하며 대화식 전환 논리를 담당합니다.
-
ZoomDismissalInteractionController 에는 사용자가 팬 제스처를 수행 할 때마다 호출되는 메서드가 있으며 현재 팬 동작의 상태에 따라 사진의 애니메이션을 제어합니다.
class ZoomDismissalInteractionController: NSObject {
... // 사용자가 팬 동작을 수행 할 때마다 호출됩니다 func didPanWith(gestureRecognizer: UIPanGestureRecognizer) { // 원본 이미지의 중심점 let anchorPoint = CGPoint(x: fromReferenceImageViewFrame.midX, y: fromReferenceImageViewFrame.midY) // 이미지 변형 let translatedPoint = gestureRecognizer.translation(in: fromReferenceImageView) // 이미지의 수직 이동 let verticalDelta = translatedPoint.y < 0 ? 0 : translatedPoint.y let backgroundAlpha = backgroundAlphaFor(view: fromVC.view, withPanningVerticalDelta: verticalDelta) let scale = scaleFor(view: fromVC.view, withPanningVerticalDelta: verticalDelta) // 팬 제스처의 변형에 따라 이미지 크기 업데이트 transitionImageView.transform = CGAffineTransform(scaleX: scale, y: scale) // 애니메이션을 사용할 이미지의 위치 계산 let newCenter = CGPoint(x: anchorPoint.x + translatedPoint.x, y: anchorPoint.y + translatedPoint.y - transitionImageView.frame.height * (1 - scale) / 2.0) transitionImageView.center = newCenter // 스케일에 따른 대화 형 전환 업데이트 transitionContext.updateInteractiveTransition(1 - scale)
if gestureRecognizer.state == .ended {
let velocity = gestureRecognizer.velocity(in: fromVC.view)
// If the user take the image upwards, cancel the transition
if velocity.y < 0 || newCenter.y < anchorPoint.y {
// Cancel
}
// Animate
}
}
}
extension ZoomDismissalInteractionController: UIViewControllerInteractiveTransitioning {
func startInteractiveTransition(_ transitionContext: UIViewControllerContextTransitioning) {
self.transitionContext = transitionContext
let containerView = transitionContext.containerView
guard let animator = self.animator as? ZoomAnimator,
let fromVC = transitionContext.viewController(forKey: .from),
let toVC = transitionContext.viewController(forKey: .to),
let fromReferenceImageViewFrame = animator.fromDelegate?.referenceImageViewFrameInTransitioningView(for: animator),
let toReferenceImageViewFrame = animator.toDelegate?.referenceImageViewFrameInTransitioningView(for: animator),
let fromReferenceImageView = animator.fromDelegate?.referenceImageView(for: animator)
else {
return
}
animator.fromDelegate?.transitionWillStartWith(zoomAnimator: animator)
animator.toDelegate?.transitionWillStartWith(zoomAnimator: animator)
self.fromReferenceImageViewFrame = fromReferenceImageViewFrame
self.toReferenceImageViewFrame = toReferenceImageViewFrame
let referenceImage = fromReferenceImageView.image!
containerView.insertSubview(toVC.view, belowSubview: fromVC.view)
if animator.transitionImageView == nil {
let transitionImageView = UIImageView(image: referenceImage)
transitionImageView.contentMode = .scaleAspectFill
transitionImageView.clipsToBounds = true
transitionImageView.frame = fromReferenceImageViewFrame
animator.transitionImageView = transitionImageView
containerView.addSubview(transitionImageView)
}
}
댓글