티스토리 뷰
iOS Expert | SideMenu에 gesture 추가하기 UIPercentDrivenInteractionTransitioning
글그리 2020. 12. 8. 13:01아 정말 이름이 너무 길다. 이름만 봐도 무슨 일을 하는지 알 수 있어서 좋지만 이렇게 긴 영어단어를 띄어쓰기도 없이 쓰면 줄바꿈이 엉망이 된다. 아무튼 이 전 포스팅에서 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였다.
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
'Programming > IOS' 카테고리의 다른 글
IOS Beginner | 끌어내려서 새로고침 UIActivityIndicatorView (0) | 2020.12.10 |
---|---|
IOS Beginner | 서버로부터 데이터 수신하기 URLSession (0) | 2020.12.09 |
iOS Expert | SideMenu 만들기 UIViewControllerTransitioningDelegate (0) | 2020.12.07 |
iOS Expert | UIScrollView StickyHeader 만들기 (0) | 2020.12.03 |
IOS Beginner | MVC 패턴으로 분리 (0) | 2020.12.01 |
- Total
- Today
- Yesterday
- C++
- OS
- SwiftUI
- C
- 국내여행
- winsock
- 수학
- SHADER
- DesignPattern
- game
- 운영체제
- Cocos2d-x
- machine learing
- 알고리즘
- SOCKET
- 데이터베이스
- Git
- Spring
- JSP
- rxswift
- swift
- database
- 드라마
- scala
- C/C++
- ios
- ue4
- mongoDB
- Java
- 자료구조
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |