이번에는 Airbnb앱을 진입하자마자 보이는 검색창을 눌렀을때 보여지는 트랜지션 효과를 따라 만들려고합니다!
막상 앱을 사용할때는 생각없이 사용해서 그런지.. 그냥 무심코 넘어갔는데 이거를 어떻게 구현했지?? 라고 생각하면서 다시보게되니 막막하더군요..
내가 아는 트랜지션은 오른쪽에서 왼쪽으로 들어오는 pushViewController와 아래에서 위로 올라오는 present뿐인데..
그래서 커스텀 뷰 컨트롤러 트랜지션에 대해서 먼저 찾아봤습니다.
참고 링크
유튜브에 튜토리얼식으로 잘 나와있더군요 🙇♂️ 저는 아래 영상과 링크들을 보고 도움을 많이 얻었어요!
아래 튜토리얼 영상을 그대로 따라해보는것도 좋을 것 같습니다.
- iOS Swift Tutorial: Create a Circular Transition Animation (Custom UIViewController Transitions)
- CREATE ZOOMING IMAGE TRANSITION in iOS - UICollectionViewController, Custom UICollectionViewCell
- [Apple] 공식문서 UIViewControllerTransitioningDelegate
- [Apple] 공식문서 UIViewControllerAnimatedTransitioning
트랜지션 동작 원리
뷰 컨트롤러의 트랜지션은 UIViewControllerAnimatedTransitioningDelegate 프로토콜 구현을 통해 동작합니다.
프로토콜을 구현하고 UIViewController의 transitioningDelegate 로 채책만 해주면 됩니다!
채택은 쉽지만..이제 트랜지션을 어떻게 동작시키냐 만드는것이 문제군요..🤔
먼저 UIViewControllerAnimatedTransitioningDelegate 구현하기 위한 방법을 보기 위해 공식문서를 먼저 봅시다.
UIViewControllerAnimatedTransitioningDelegate에 UIViewController가 presenting될 때, dissmiss될 때 어떤 AnimatedTransitioning을 사용할건지 물어보는 함수들이네요!
그러면 AnimatedTransitioning를 만들어서 presenting될 때, dismiss될 때 반환해주면 되겠네요!
그럼 다음으로는 AnimatedTransitioning를 어떻게 만들어야하는지..공식문서를 또 타고 들어가봅니다..
프로토콜을 구현하기 위해서 필수로 만들어야하는 함수가 두가지가 있군요.
- animateTransition(using: UIViewControllerContextTransitioning) : 실제로 애니메이션을 구현하는 곳
- transitionDuration(using: UIViewControllerContentTransitioning?) -> TimeInterval : 애니메이션이 실행되는 시간
그럼 AnimatedTransitioning 부터 하나씩 만들어봅시다!
AnimatedTransitioning 만들기
실제 애니메이션을 만들기 전에 어떠한 방식으로 애니메이션이 되게 할것인지 설계를 먼저 해봅시다!
트랜지션 전 화면의 상태는 상단에 위치를 알려주는 박스가 있습니다.
해당 박스가 전체화면으로 커지면서 우리가 이동해야하는 화면이 보여져야할겁니다.
그래서 저는 마스킹을 사용해서 박스가 커지면서 다음 화면이 보이게 할 예정입니다.
위 그림을 보면 애니메이션이 어떻게 동작하는지 쉽게 이해할 수 있습니다.
From View(이동 전 화면)는 그대로 있고 그 위에 마스크 뷰를 만들어 놓습니다. 마스크 뷰는 이동할 화면을 마스킹해주는 뷰입니다. 이 뷰는 처음에 주소창만큼의 frame을 가지고 있다가. 전체 화면으로 frame이 커지는 애니메이션을 줄겁니다. 그러면 새로 보여질 화면이 점점 보여지는 효과가 나타나겠죠??
그리고 이동할 화면을 추가해주는데, alpha값을 0.0인 상태로 추가해줍니다.(보이지 않는 상태) 그리고 마스크 뷰의 subView로 추가해줘서 마스킹이 되도록 적용합니다. 이동할 화면의 alpha값은 0.0에서 1.0으로 변하는 fade in 애니메이션을 줄 예정입니다.
반대로 Dismiss일 경우에는 위 순서를 반대로 적용하면 됩니다!
- 마스킹 뷰는 전체화면 frame에서 주소창 만큼의 frame으로 scale down
- From View의 Alpha값이 1.0인 상태에서 0.0으로 fade out
- 애니메이션이 끝나면 마스킹 뷰 제거
// SearchTransition.swift
import UIKit
class SearchTransition: NSObject {
let duration = 0.3
var maskView = UIView().then {
$0.backgroundColor = .white
$0.layer.cornerRadius = 16
}
var maskOriginalFrame = CGRect.zero
var transitionMode: PresentTransitionMode = .present
enum PresentTransitionMode: Int {
case present, dismiss
}
}
extension SearchTransition: UIViewControllerAnimatedTransitioning {
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return self.duration
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
let containerView = transitionContext.containerView
if self.transitionMode == .present {
if let presentedView = transitionContext.view(forKey: UITransitionContextViewKey.to) {
presentedView.alpha = 0
self.maskView.addSubview(presentedView)
containerView.addSubview(self.maskView)
UIView.animate(withDuration: self.duration, delay: 0, options: .curveEaseInOut) {
self.maskView.frame = presentedView.frame
presentedView.alpha = 1
} completion: { isSuccess in
transitionContext.completeTransition(isSuccess)
}
}
} else {
if let returningView = transitionContext.view(forKey: UITransitionContextViewKey.from) {
UIView.animate(withDuration: duration, delay: 0, options: .curveEaseInOut) {
self.maskView.frame = self.maskOriginalFrame
returningView.alpha = 0
} completion: { isSuccess in
returningView.removeFromSuperview()
self.maskView.removeFromSuperview()
transitionContext.completeTransition(isSuccess)
}
}
}
}
}
Present할때의 코드를 먼저 보면
if self.transitionMode == .present {
if let presentedView = transitionContext.view(forKey: UITransitionContextViewKey.to) {
presentedView.alpha = 0
self.maskView.addSubview(presentedView)
containerView.addSubview(self.maskView)
UIView.animate(withDuration: self.duration, delay: 0, options: .curveEaseInOut) {
self.maskView.frame = presentedView.frame
presentedView.alpha = 1
} completion: { isSuccess in
transitionContext.completeTransition(isSuccess)
}
}
}
transitionContext.view(forKey: UITransitionContextViewKey.to) 을 사용하면 이동할 View를 가져올 수 있습니다.
가져와서 위에 적힌 순서대로 로직을 작성해줍니다.
- presentedView의 Alpha를 0.0(투명)으로 설정(처음에는 안보여야하니깐)
- 마스크 뷰에 presentView를 하위 뷰로 넣어서 마스킹 설정
- 애니메이션 실행하면서 presentedView의 alpha를 1.0으로 만들어서 fade in 효과 적용
- 애니메이션 실행하면서 마스크 뷰의 frame을 presentedView 프레임과 동일하게설정해서 커지는 효과 적용
애니메이션이 끝나고 나서는 transitionContext.completeTransition을 꼭 불러줍니다! transitionContext.completeTransition는 시스템에게 트랜지션이 끝났음을 알려주는 함수입니다.
반대로 dismiss할 때는
if let returningView = transitionContext.view(forKey: UITransitionContextViewKey.from) {
UIView.animate(withDuration: duration, delay: 0, options: .curveEaseInOut) {
self.maskView.frame = self.maskOriginalFrame
returningView.alpha = 0
} completion: { isSuccess in
returningView.removeFromSuperview()
self.maskView.removeFromSuperview()
transitionContext.completeTransition(isSuccess)
}
}
- 애니메이션 실행하면서 마스크 뷰의 프레임을 원래 주소창 만하게 scale down
- 애니메이션 실행하면서 사라질 뷰의 alpha값을 0으로 설정해서 fade out 효과 적용
- 애니메이션 끝나고 마스크뷰와 사라질 뷰를 removeFromSuperView() 호출
이렇게 적용하면 애니메이션에 대한 로직은 구현되었습니다!
UIViewControllerTransitioningDelegate 적용
애니메이션 구현은 끝났으니 ViewController에 적용해봅시다!
// HomeVC.swift
import UIKit
class HomeVC: UIViewController {
private lazy var homeView = HomeView(frame: self.view.frame)
let transition = SearchTransition()
override func viewDidLoad() {
super.viewDidLoad()
view = homeView
...
}
private func showSearchAddress() { // 주소 검색 화면 present 시켜주는 함수
let searchAddressVC = SearchAddressVC.instacne()
searchAddressVC.transitioningDelegate = self
self.present(searchAddressVC, animated: true, completion: nil)
}
...
}
extension HomeVC: UIViewControllerTransitioningDelegate {
func animationController(
forPresented presented: UIViewController,
presenting: UIViewController,
source: UIViewController
) -> UIViewControllerAnimatedTransitioning? {
self.transition.transitionMode = .present
self.transition.maskView.frame = self.homeView.addressContainerView.frame
return self.transition
}
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
self.transition.transitionMode = .dismiss
self.transition.maskOriginalFrame = self.homeView.addressContainerView.frame
return self.transition
}
}
실제로 ViewController에서 present시켜주는것은 간단합니다.
present시켜줄 ViewController를 생성하고 해당 ViewController의 transitioningDelegate를 채택해주면 됩니다!
UIViewControllerTransitioningDelegate를 구현할때에는 presnet될때, dismiss될때 transitionMode와 maskView의 frame크기만 설정해주면 끝!
위 패턴을 사용해서 다양한 트랜지션 효과를 만들볼수도 있을 것 같다는 생각이 듭니다!
더 멋진 트랜지션을 또 한번 만들어봐야겠네요!🙌
'iOS > 힙한 UI 따라 만들기' 카테고리의 다른 글
iOS 힙한 UI 따라 만들기 Ep.03 "Frip" 홈 화면 UICompositionalLayout으로 만들기👋 (0) | 2022.09.19 |
---|---|
iOS 힙한 UI 따라 만들기 Ep.01 "에이블리, 배민" 헤더가 고정되는 테이블 뷰 만들기👋 (8) | 2021.02.17 |