관리 메뉴

김종권의 iOS 앱 개발 알아가기

[iOS - swift] present, dismiss, pop, push 화면전환 애니메이션 커스텀 방법 (CATransition, CATransitionType) 본문

iOS 응용 (swift)

[iOS - swift] present, dismiss, pop, push 화면전환 애니메이션 커스텀 방법 (CATransition, CATransitionType)

jake-kim 2023. 1. 9. 08:30

화면전환 애니메이션 커스텀 방법

  • UIViewControllerTransitioningDelegate를 사용한 방법
    • 델리게이트를 구현하여 여러곳에서 공통적인 애니메이션이 사용될 경우, 한번 구현해놓으면 다른곳에서도 쓰기 쉽기 때문에 재활용성이 높음
    • * UIViewControllerTransitioningDelegate를 이용한 화면전환 애니메이션 커스텀 방법은 이전 포스팅 글 참고
  • CATransition을 사용한 방법
    • 여러곳에서 사용되지 않고 특정 화면에서만 사용하는 경우, transition 인스턴스를 만들어서 적용

CATransition이란?

  • Core Animation Transition은 단어 그대로 UIKit 보다 더 낮은 레벨인 Core Animation의 한 종류이며 특정 값을 세팅하여 transition 애니메이션을 커스텀 할 수 있는 인스턴스
  • UIView가 가지고 있는 layer의 add(_:forKey:)에 추가하여 애니메이션 커스텀이 가능
open class CALayer : NSObject, NSSecureCoding, CAMediaTiming {
    open func add(_ anim: CAAnimation, forKey key: String?)
}
  • 정리하여 트랜지션 커스텀 원리는, 원하는 형태의 트랜지션 애니메이션 형태를 CATransition 인스턴스로 정의한 다음, UIView의 layer에 add하여 적용

ex) UILabel에 트랜지션 넣기

오른쪽에서 등장하는 UILabel

  • 버튼을 만든 후 이 버튼의 layer에 CATransition() 인스턴스를 add해주는 방법
class ViewController: UIViewController {
    private let button: UIButton = {
        let button = UIButton()
        button.setTitle("present", for: .normal)
        button.setTitleColor(.systemBlue, for: .normal)
        button.setTitleColor(.blue, for: .highlighted)
        button.translatesAutoresizingMaskIntoConstraints = false
        return button
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        
        let transition = CATransition()
        transition.duration = 0.25
        transition.type = .push
        transition.subtype = .fromRight
        transition.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
        button.layer.add(transition, forKey: kCATransition)
        
        view.addSubview(button)
        
        NSLayoutConstraint.activate([
            button.centerYAnchor.constraint(equalTo: view.centerYAnchor),
            button.centerXAnchor.constraint(equalTo: view.centerXAnchor),
        ])
    }
}

CATransition에서 제공하는 프로퍼티

  • type
    • fade: 흐려지는 효과
    • moveIn: 기존 콘텐츠 위에 새로운 콘텐츠가 덮이는 형태
    • push: 새로운 콘텐츠가 기존 콘텐츠를 밀면서 전환되는 형태
    • reveal: subtype에 의해 지정된 방법으로 점진적 등장하는 형태 (subtype이란 CATransition에 있는 또다른 프로퍼티이며 아래에서 계속)

https://developer.apple.com/documentation/quartzcore/catransitiontype

  • subtype: 트랜지션 시작 위치

https://developer.apple.com/documentation/quartzcore/catransitionsubtype

CATransition을 사용하여 화면전환 애니메이션 커스텀하기

  • 위에서 살펴본대로 view.layer에 CATransition 인스턴스를 add하여 화면전환도 커스텀 가능

ex) present, dismiss는 Navigation처럼 만들고, Navigation은 present, dismiss처럼 만들기

  • 버튼에 addTarget
button.addTarget(self, action: #selector(openVC2), for: .touchUpInside)

    @objc private func openVC2() {
        let vc = VC2()
        vc.modalPresentationStyle = .fullScreen
        let naviVC = UINavigationController(rootViewController: vc)
        
        // 이곳에 CATransition 구현
    }
  • modal의 화면전환을 담당하는 인스턴스는 layer window이므로 window의 layer에 add하여 트랜지션 적용
@objc private func openVC2() {
    let vc = VC2()
    vc.modalPresentationStyle = .fullScreen
    let naviVC = UINavigationController(rootViewController: vc)
    
    let transition = CATransition()
    transition.duration = 0.25
    transition.type = .push
    transition.subtype = .fromRight
    transition.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
    view.window?.layer.add(transition, forKey: kCATransition)
    present(naviVC, animated: false, completion: nil)
}
  • extension을 이용하여 CATransition을 미리 정의하여 사용
// 출처: https://stackoverflow.com/questions/51675063/how-to-present-view-controller-from-left-to-right-in-ios
extension CATransition {
    func segueFromBottom() -> CATransition {
        duration = 0.375 //set the duration to whatever you'd like.
        timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
        type = .moveIn
        subtype = .fromTop
        return self
    }
    
    func segueFromTop() -> CATransition {
        duration = 0.375 //set the duration to whatever you'd like.
        timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
        type = .moveIn
        subtype = .fromBottom
        return self
    }
    
    func segueFromLeft() -> CATransition {
        duration = 0.1 //set the duration to whatever you'd like.
        timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
        type = .moveIn
        subtype = .fromLeft
        return self
    }
    
    func popFromRight() -> CATransition {
        duration = 0.1 //set the duration to whatever you'd like.
        timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
        type = .reveal
        subtype = .fromRight
        return self
    }
    
    func popFromLeft() -> CATransition {
        duration = 0.1 //set the duration to whatever you'd like.
        timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
        type = .reveal
        subtype = .fromLeft
        return self
    }
}
  • 나머지, VC2와 VC3에도 트랜지션 적용
class VC2: UIViewController {
    private let stackView: UIStackView = {
        let stackView = UIStackView()
        stackView.axis = .vertical
        stackView.translatesAutoresizingMaskIntoConstraints = false
        return stackView
    }()
    private let pushButton: UIButton = {
        let button = UIButton()
        button.setTitle("push", for: .normal)
        button.setTitleColor(.systemBlue, for: .normal)
        button.setTitleColor(.blue, for: .highlighted)
        button.addTarget(self, action: #selector(pushVC), for: .touchUpInside)
        button.translatesAutoresizingMaskIntoConstraints = false
        return button
    }()
    private let dismissButton: UIButton = {
        let button = UIButton()
        button.setTitle("dismiss", for: .normal)
        button.setTitleColor(.systemBlue, for: .normal)
        button.setTitleColor(.blue, for: .highlighted)
        button.addTarget(self, action: #selector(dismissVC), for: .touchUpInside)
        button.translatesAutoresizingMaskIntoConstraints = false
        return button
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.backgroundColor = .white
        view.addSubview(stackView)
        [pushButton, dismissButton]
            .forEach(stackView.addArrangedSubview(_:))
        
        NSLayoutConstraint.activate([
            stackView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
            stackView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
        ])
    }
    
    @objc private func pushVC() {
        navigationController?.view.layer.add(CATransition().segueFromBottom(), forKey: kCATransition)
        navigationController?.pushViewController(VC3(), animated: false)
    }
    
    @objc private func dismissVC() {
        let transition = CATransition()
        transition.duration = 0.25
        transition.type = .push
        transition.subtype = .fromLeft
        transition.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
        view.window?.layer.add(transition, forKey: kCATransition)
        dismiss(animated: false)
    }
}

class VC3: UIViewController {
    private let popButton: UIButton = {
        let button = UIButton()
        button.setTitle("pop", for: .normal)
        button.setTitleColor(.systemBlue, for: .normal)
        button.setTitleColor(.blue, for: .highlighted)
        button.addTarget(self, action: #selector(popVC), for: .touchUpInside)
        button.translatesAutoresizingMaskIntoConstraints = false
        return button
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.backgroundColor = .white
        view.addSubview(popButton)
        NSLayoutConstraint.activate([
            popButton.centerYAnchor.constraint(equalTo: view.centerYAnchor),
            popButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
        ])
    }
    
    @objc private func popVC() {
        navigationController?.view.layer.add(CATransition().segueFromTop(), forKey: kCATransition)
        navigationController?.popViewController(animated: false)
    }
}

* 전체코드: https://github.com/JK0369/ExCATransition

 

* 참고

https://stackoverflow.com/questions/51675063/how-to-present-view-controller-from-left-to-right-in-ios

https://developer.apple.com/documentation/quartzcore/catransitionsubtype

https://developer.apple.com/documentation/quartzcore/catransitiontype

 

Comments