티스토리 뷰

아 정말 이름이 너무 길다. 이름만 봐도 무슨 일을 하는지 알 수 있어서 좋지만 이렇게 긴 영어단어를 띄어쓰기도 없이 쓰면 줄바꿈이 엉망이 된다. 아무튼 이 전 포스팅에서 UIViewControllerTransitioningDelegate를 사용하면서 animationController 뿐 아니라 interactionController도 있다고 했는데 바로 그 interactionController로 사용할 수 있는 class다. 이 class(protocol이 아니다)를 가지고 버튼을 누를 때 말고 pan gesture를 할 때도 side menu가 보일 수 있도록 만들어 보자.

dismiss 할 때는 이전과 동일하게 dimmingView를 탭 하면 dismiss가 일어나도록 하고, present 할 때만 새로운 interaction을 추가할 것이다.

 

 

 

UIPercentDrivenInteractionTransitioning

이 class는 UIViewControllerInteractiveTransitioning protocol을 채택한 클래스로 UIKit에서 기본적으로 제공하는 class이다. 사실 이름만 보고 UIViewControllerInteractiveTransitioning이 progress를 처리할 수 있는 protocol 인 줄 알았다. Interactive니까, 재생만 하면 시간의 흐름에 따라 transition을 처리하는 게 아니라 update같은 함수를 통해 전체 진행도를 update하면서 transition을 처리하는 기능을 생각했다. 하지만 생각했던 것과 달리 그보다 더 추상적인 개념인 것 같고, 이 protocol로 정의한 UIPercentDrivenInteractionTransition이 생각했던 위와 같은 기능을 하는 class였다.

 

animation 진행도를 제어할 수 있다.

 

SidePresentationAnimator로 이동해서 presentAnimator, dismissAnimator 이렇게 2개의 class로 나누자. 사실 present나 dismiss가 재생 순서만 반대로 될 뿐, 똑같은 animation을 사용하기 때문에 굳이 두 class로 나눌 필요는 없지만 사용하는 변수나 수행하는 동작이 조금은 다르기 때문에 미리 분리해 두는 게 편할 것 같다.

SidePresentAnimator는 도중에 취소됐을 때 따로 처리하기 위해서 isCanceled라는 플래그를 가진다. SideDismissAnimator는 dismiss일 때만 사용되는 것이 명확해졌기 때문에 isPresent 플래그가 필요없어졌다.

 

class SidePresentAnimator : UIPercentDrivenInteractionTransitioning, UIViewControllerAnimatedTransitioning {
	var isCanceled: Bool = false

	override func cancel() {
		isCanceld = true
		super.cancel()
	}

	func reset() -> SidePresentAnimator {
		isCanceled = false
		return self
	}
}

class SideDismissAnimator : NSObject, UIViewControllerAnimatedTransitioning {
	// no need isPresent flag

	// ...
}

 

present animator에도 같은 방법으로 animateDuration, animateTransition을 추가해보자. 이 때 주의해야 할 점은 interactive 도중에 present를 취소할 수 있다는 점인데, 이 때 취소를 처리해 주어야만 UIKit에서 추가한 transitionView 등이 깔끔하게 지워지면서 취소된다.

 

// SidePresentAnimator
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
	guard let controller = transitionContext.viewController(forKey: .to) else {
		return
	}

	transitionContext.contentView.addSubview(controller.view)

	let presentFrame = transitionContext.finalFrame(for: controller)
	var dismissFrame = presentFrame
	dismissFrame.origin.x = -presentFrame.width

	controller.view.frame = dismissFrame
	UIView.animate(withDuration: transitionDuration(using: transitionContext), animation: {
		controller.view.frame = presentFrame
	}, completion: {bFinished in
		transitionContext.completeTransition(!isCanceled && bFinished)
	})
}

 

transitionContext.completeTransition에 isCanceled를 대입해서 취소되었을 때는 UIKit이 취소를 처리할 수 있도록 한다. 당연히 cancel()을 호출하면 context의 bFinished에 false가 들어올 줄 알았지만 어떻게 끝내도 항상 true가 들어와서 임의로 처리하도록 했다.

다시 delegate로 돌아가서 새롭게 만든 animator들을 생성할 수 있도록 수정하자.

UIKit는 present가 실행될 때transitionDelegate를 통해 animationController가 있는지 물어보고, 그 다음 interactionControllerForPresentation가 있는지 물어본다. 이 결과가 nil일 경우 animationController만 사용하지만 뭔가 반환되는 animator가 있을 경우 그 animator를 사용해서 transition한다.

side menu는 버튼을 누르냐, pan gesture를 하냐에 따라 interactive가 있을 수도 있고, 없을 수도 있기 때문에 이걸 구분할 수 있는 flag를 추가한다.

 

class SideTransitionDelegate: NSObject {
    var sideWidth: CGFloat = 100
    var presentAnimator = SidePresentAnimator()
    var isInteractive: Bool = false
    
    init(sideWidth: CGFloat) {
        self.sideWidth = sideWidth
        
        super.init()
    }
}

 

그리고 이렇게 만든 flag에 따라 animator를 생성하도록 수정한다.

 

// SideTransitionDelegate
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
	return presentAnimator.reset()
}

func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
	return SideDismissAnimator()
}
    
func interactionControllerForPresentation(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
	if isInteractive {
		return presentAnimator.reset()
	} else {
		return nil
	}
}

 

이제 delegate의 isInteractive값에 따라 그냥 animation을 재생할 수도, interativeTransition을 할 수도 있게 되었다.

 

 

 

UIScreenEdgePanGestureRecognizer

화면의 가장자리에서 pan gesture를 할 때 알아차릴 수 있는 recognizer를 추가한다.

 

let recognizer = UIScreenEdgePanGestureRecognizer(target: self, action: #selector(interactSideMenu(_:))
view.addGestureRecognizer(recognizer)

 

pan gesture는 마우스로 치면 drag 입력으로, 입력이 되고있는 매 순간 .begin, .changed, end 등 state가 변경되고, translation 으로 특정 view에서 시작점 기준 현재 위치를 얻을 수 있다. gesture가 입력되는 동안 progress를 0~1 사이의 값으로 계산해서 animatore를 update시켜주면 터치 입력에 따라 반응하는 transition효과를 만들 수 있다.

 

@objc func panGesture(_ recognizer: UIScreenEdgePanGestureRecognizer) {
	let location = recognizer.translation(in: view)
	var progress: CGFloat = location.x / 100
	// clamp
	if progress > 1.0 { progress = 1.0 } 
	else if progress < 0.0 { progress = 0.0 }

	switch recognizer.state {
		case .began:
			let side = SideMenuViewController()
			sideTransitionDelegate.isInteractive = true
			side.transitioningDelegate = sideTransitionDelegate
			side.modalPresentationStyle = .custom
			present(side, animated: true, completion: nil)
		case .changed:
			sideTransitionDelegate.presentAnimator.update(progress)
		case .cancelled:
			sideTransitionDelegate.presentAnimator.finish()
		case .ended:
			if progress > 0.5 {
				sideTransitionDelegate.presentAnimator.finish()
			} else {
				sideTransitionDelegate.presentAnimator.cancel()
			}
		default: break
	}
}

 

 

 

Result

여전히 side menu에 아무것도 넣지 못했다.

 

ScreenEdgePanGesture를 사용하려면 위처럼 simulator의 device bezel이 보이도록 설정해 주어야 한다.

 

전체 소스코드는 아래 링크에서 받아볼 수 있다.

https://github.com/eastroot1590/TodoChallenge/tree/4c1f1f43233ed2d484e817453619c79dcd589a52

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/12   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31
글 보관함