Danny의 iOS 컨닝페이퍼
article thumbnail

[Swift/ TIL #32] 2023 / 06 / 26 ~ 2023 / 06 / 27

오늘은 탭바 아이템을 클릭할 때, 슬라이드 효과 같은 애니메이션 효과를 만들어 보려고 합니다.

(뷰 컨트롤러 전환 애니메이션)

 

 

탭바 애니메이션

예전에 만들어 만들어 둔, 탭바에 애니메이션을 적용시켜 보겠습니다.

 

일단 탭바 애니메이션 적용을 위해, 처음으로 할 작업은 탭바 델리게이트를 채택해 주는 겁니다.

class WorldTimeViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
   
        self.tabBarController?.delegate = self
        // 커스텀으로 탭바컨트롤러를 만들어줬으면, 이런식으로 하겠죠.
        // self.delegate = self
    }
}

 

애니메이션 방법을 찾아보니까, 정말 여러 가지 방법이 있더라고요.

 

그중, 간편히 만들 수 있는 방법들을 알아봅시다.

 

 

UIView의 transition 메서드 사용

기본으로 내장돼있는 메서드라 그런지 사용방법이 엄청 간단합니다. 

현재 뷰에서 다른 뷰로 전환 시, 전환 애니메이션을 간단히 처리할 수 있게 도와줍니다!

UIView.transition(
    from: UIView,
    to: UIView,
    duration: TimeInterval,
    options: UIView.AnimationOptions,
    completion: ((Bool) -> Void)?
)

// from: 현재 뷰(자동으로 슈퍼뷰에서 제거 됨)
// to: 넘어갈 뷰(자동으로 슈퍼뷰에 추가 됨)
// options: 애니메이션 옵션을 설정

 

이 메서드를 통해서 현재 보이는 뷰나타날 뷰에 대하여, 애니메이션 효과를 만들어 줄 수 있습니다.

 

탭바 컨트롤러 델리게이트에는 shouldSelect라는 메서드가 있는데,

이 메서드의 파라미터로는 탭바 컨트롤러와 나타날 뷰컨트롤러가 존재합니다.

(여기서 탭바 컨트롤러로 현재 뷰, 나타날 뷰컨트롤러를 통해 나타날 뷰에 접근할 수 있어요)

 

shouldSelect 메서드를 사용하면, 간편하게 transition 메서드를 사용할 수 있겠죠.

extension WorldTimeViewController: UITabBarControllerDelegate {
    
    func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
        // 현재 뷰와 선택한 뷰를 뽑아 줍시다. 뷰가 존재 하지 않으면 탭바 전환 없음 (false)으로 만들어 줌
        guard let fromView = tabBarController.selectedViewController?.view,
              let toView = viewController.view else { return false }
        
        // 현재 뷰와 바뀔 뷰가 같다면 동작 안함. (shouldSelect 메서드는 클릭할 때마다 동작하므로 멈춰주는 조건이 필요함)
        if fromView == toView {
            return false
        } else {
            // options에서 뷰 전환 애니메이션은 transition으로 시작하는 값을 사용
            UIView.transition(from: fromView, to: toView, duration: 0.5, options: .transitionCrossDissolve)
            return true
        }
    }
}

 

왼쪽부터 transitionCrossDissolve(녹는? 효과), transitionCurlUp(감아올리는 효과),

transitionFlipFromLeft(왼쪽에서 오른쪽으로 뒤집는 효과)

 

그리고 transition 메서드는 다른 애니메이션이 실행 중인 경우, 다른 애니메이션 끝나고 동작한다고 하는데...

테스트를 해보니, 동시에 실행이 되네요.

(흠, 잘못 이해했나...)

 

아무튼, 이걸 응용해 두 가지 애니메이션을 사용하면, 이런 식으로도 효과를 만들 수도 있더라고요.

(응용까지는 아닌 것 같고 그냥 애니메이션이 중첩이 되네요 ㅎㅎ)

func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
    guard let fromView = tabBarController.selectedViewController?.view,
          let toView = viewController.view else { return false }

    if fromView == toView {
        return false
    } else {
        // 270도 회전 및 크기 0.1배로 줄이기
        toView.transform = CGAffineTransform(rotationAngle: .pi*2/3).scaledBy(x: 0.1, y: 0.1)
        UIView.animate(withDuration: 0.5) {
            // 처음상태로 만들기
            toView.transform = .identity
        }
        // 녹는 효과
        UIView.transition(from: fromView, to: toView, duration: 0.8, options: .transitionCrossDissolve)

        return true
    }
}

 

그런데 전환(transition) 애니메이션 옵션의 종류가 한정적이다 보니,

만약, 다른 전환 애니메이션을 구현하고 싶다면, 전환 애니메이션을 직접 구현해 줘야 합니다.

(예, 좌우로 슬라이드 되는 애니메이션 같은 경우, 확대 등)

 

그럼 직접 구현을 하는 방법을 알아봐야겠죠?

바로, UIViewControllerAnimatedTransitioning를 통해 전환 애니메이션을 구현해 줄 수 있습니다.

 

 

UIViewControllerAnimatedTransitioning

UIViewControllerAnimatedTransitioning은 직접 뷰 전환 애니메이션을 만들어 사용할 수 있는 프로토콜입니다.

(현재 뷰나타날 뷰를 갖고 애니메이션을 효과를 만들 수 있습니다.)

 

그리고 탭바 컨트롤러 델리게이트의 메서드들이 보통 뷰 전환을 다뤄서 그런지,

UIViewControllerAnimatedTransitioning를 이용하는 메서드가 존재하더라고요.

 

Stackoverflow 를 참고하여 만들었습니다. 설명은 주석을 참고해 주세요.

class SlideTransitionAnimator: NSObject, UIViewControllerAnimatedTransitioning {
    
    // 각 뷰컨트롤러의 인덱스를 구하기 위해 사용
    let viewControllers: [UIViewController]?
    // 전환 애니메이션 시간
    let transitionDuration: Double = 0.5
    
    init(viewControllers: [UIViewController]?) {
        self.viewControllers = viewControllers
    }
    
    // 필수 메서드 (전환 애니메이션의 지속 시간)
    func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        return TimeInterval(transitionDuration)
    }
    
    // 필수 메서드 (전환 애니메이션 효과를 정의)
    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
        
        guard let fromVC = transitionContext.viewController(forKey: .from),
              let fromView = fromVC.view,
              let fromIndex = getIndex(forViewController: fromVC),
              let toVC = transitionContext.viewController(forKey: .to),
              let toView = toVC.view,
              let toIndex = getIndex(forViewController: toVC)
        else {
            transitionContext.completeTransition(false)
            return
        }
        
        let frame = transitionContext.initialFrame(for: fromVC)
        var fromFrameEnd = frame
        var toFrameStart = frame
        // 탭바컨트롤러의 인덱스를 통해 x축으로 움직일 방향을 정해줌
        // 바뀔 뷰 > 현재 뷰 == 왼쪽으로 이동, 바뀔 뷰 < 현재 뷰 == 오른쪽으로 이동
        fromFrameEnd.origin.x = toIndex > fromIndex ? -frame.width : +frame.width
        toFrameStart.origin.x = toIndex > fromIndex ? +frame.width : -frame.width
        
        // 예를들어, 바뀔 뷰 > 현재 뷰라면 현재 toView.orisin.x 위치는 보이는 영역 바깥쪽(오른쪽)에 위치함.
        // 그리고 아래의 애니메이션을 통해 위치를 0으로 이동 시켜줌 (왼쪽으로 이동 시킴)
        toView.frame = toFrameStart
        
        DispatchQueue.main.async {
            // ⭐️ containerView는 애니메이션 실행되는 동안 나타나는 중간 뷰(틀)라고 생각하면 됩니다.
            // UIView.transition 메서드에서는 자동으로 슈퍼뷰에서 추가 및 제거가 됐지만,
            // 여기선 우리가 직접 뷰를 추가 및 제거 해줘야 합니다.
            transitionContext.containerView.addSubview(toView)
            UIView.animate(withDuration: self.transitionDuration) {
                fromView.frame = fromFrameEnd
                toView.frame = frame
            } completion: { success in
                // 슈퍼뷰에서 제거
                fromView.removeFromSuperview()
                // 필수적으로 전환이 완료 됬다는 시스템에 알려줘야 한다고 함.
                transitionContext.completeTransition(success)
            }
        }
    }
    
    // 현재 뷰컨트롤러의 인덱스 구하기 (인덱스를 통해 왼쪽, 오른쪽으로 넘길지 알아야 한다.)
    func getIndex(forViewController vc: UIViewController) -> Int? {
        guard let viewControllers = self.viewControllers else { return nil }
        for (index, viewController) in viewControllers.enumerated() {
            if viewController == vc { return index }
        }
        return nil
    }
}

 

탭바컨트롤러 델리게이트 메서드에서 SlideTransitionAnimator 생성하여 사용하면 됩니다.

extension WorldTimeViewController: UITabBarControllerDelegate {
    
    func tabBarController(_ tabBarController: UITabBarController, animationControllerForTransitionFrom fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        return SlideTransitionAnimator(viewControllers: tabBarController.viewControllers)
    }
}

 

 

다른 방법으로 슬라이드 기능 구현

처음에 사용했던, 애니메이션을 중첩시키는 방법으로도 비슷하게 구현을 할 수 있더라고요.

코드를 참고해 보시죠.

func animateTransition(to viewController: UIViewController) {
    guard let fromView = self.tabBarController?.selectedViewController?.view,
          let fromIndex = self.tabBarController?.selectedIndex,
          let toView = viewController.view,
          let toIndex = self.tabBarController?.viewControllers?.firstIndex(of: viewController)
    else { return }
    
    let screenWidth = fromView.bounds.width
    
    if fromView == toView {
        return
    } else {
        toView.frame.origin.x = toIndex > fromIndex ? screenWidth : -screenWidth
        
        UIView.animate(withDuration: 0.5, animations: {
            fromView.frame.origin.x = toIndex > fromIndex ? screenWidth : -screenWidth
            toView.frame.origin.x = 0
        })
        UIView.transition(from: fromView, to: toView, duration: 0.5, options: .transitionCrossDissolve)
    }
}

 

델리게이트에서 만들어준 메서드를 사용하면 됩니다.

func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
    animateTransition(to: viewController)
    return true
}

 

이전 뷰가 먼저 사라지는 문제가 있긴 하지만, 직접 만드는 것 보다 간편하게 사용할 수 있네요.

 

 

 

참고

https://developer.apple.com/documentation/uikit/uiviewcontrolleranimatedtransitioning

https://developer.apple.com/documentation/uikit/uiview/1622562-transition

https://stackoverflow.com/questions/44346280/how-to-animate-tab-bar

반응형
profile

Danny의 iOS 컨닝페이퍼

@Danny's iOS

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!