UIPanGestureRecognizer를 사용해 모달창 아래로 스와이프해 dismiss하기
📚 UIPanGestureRecognizer는 UIGestureRecognizer의 하위클래스로
화면을 탭 후에 상하좌우로 Drag하는 제스쳐를 통해 화면에 변화를 줄 때 사용합니다.
이 클래스의 클라이언트는 액션 메소드를 통해서
현재 제스쳐의 변화(translation)와 속도의 변화(velocity)를 줄 수 있다고 합니다.
UIPanGestureRecognizer is a concrete subclass of UIGestureRecognizer. Clients of this class can, in their action methods, query the UIPanGestureRecognizer object for the current translation of the gesture (translation(in:)) and the velocity of the translation (velocity(in:)).
UIPanGestureRecognizer 설명 속 저 2가지 메소드(공식문서 링크)가 뭘 하는지 궁금하져?
func translation(in view: UIView?) -> CGPoint
첫 번째로 translation 메소드는 파라미터로 UIView타입을 받고, CGPoint 값을 반환받습니다.
그러니까,, 우리가 손가락으로 pan(이동시킬)시킬 object들 예를 들어, imageView, button, label 들을 이동시키면 해당 객체들의 CGPoint 값을 받아온다는 뜻입니다.
func velocity(in view: UIView?) -> CGPoint
두 번째로 velocity 메소드는 파리미터로 UIView타입을 받고,, CGPoint값을 반환하는데
공식 문서를 보면 [ 초당 포인트로 표시되는 팬 제스처의 속도 ] 를 반환한대요..
이게 무슨 소리냐,,, velocity가 방향을 알려준다고 했고
초당 포인트에서 포인트는 CGPoint를 의미하는 좌표 즉, 위치를 말하는 거면
객체들을 움직일 때 초당 CGPoint 값으로 팬 제스처의 방향을 받아온다는 것이져!
그래서 이 방향을 알고 싶다!
그러면 abs() 절댓값을 통해서 수직값(y축)이 더 큰지 수평값(x축)이 더 큰지 비교해서 알아가면 됩니다..!
**
✅ 여기서 CGPoint란,,, 2차원 좌표계의 점을 포함하는 Struct입니다.
;; 뭔지 무슨 소리야~~
네 바로,, 평면 좌표에서 x,y 값을 받아오는 거에요! 수학시간에 2차 함수 그래프 그리고 (3, 6) 쓰듯이 말이죠
iOS에서는 이걸 하기 위해 CGPoint라는 Struct를 쓰는 거래요~
그리고,,,
UIPanGestureRecognizer는 State라는 int형 자료값을 갖는 Enum을 갖고 있는데
이 State 에는 꼭 알아야 할 3가지 case가 있는데여,,,,
- UIPanGestureRecognizer.State.began - 처음으로 동작이 수행되었을 때
- UIPanGestureRecognizer.State.changed - 이후에 연속적인 동작이 수행되었을 경우
- UIPanGestureRecognizer.State.ended - 사용자가 손가락을 뗄 때
여기서 began과 ended는 한 번만 호출되지만,,, changed는 여러 번 변화를 줄 때마다 호출된답니당
아무튼,,,!!!!
우리는 이걸 어디서 쉽게 볼 수 있냐면
바로바로 카카오톡 프로필 상세화면을 아래로 스와이프해 꺼줄 때 자주 사용합니다.
오늘은 이걸 구현해보도록 할 겁니다.
우선 모달화면이 필요하겠죠?
간단하게 모달 전환이 되는 화면을 만들어주세요~~! 어떻게 만드는지 모르겠다면 제가 링크를 걸어둘테니 참고해서 만들어오세옹
여기서 제일 중요한 것은 modalPresentationStyle = .overFullScreen 으로 해줘야
아래로 스와이핑이 가능하다는 점입니다!!!
modalPresentationStyle = .overFullScreen
**
✅ .fullScreen이 아닌 이유는
만약 .fullScreen인 경우 뷰의 계층들이 사라지기 때문에 현재 뷰의 투명도를 줘도 그 뒷 뷰가 없어서 아무 것도 안 보입니다.
그런데,, .overFullScreen인 경우에는 뷰의 계층이 다 있기 때문에 투명도를 주면 그 뒷 뷰가 보입니다!
( // 제가 궁금해서 실제로 해보니까 .fullScreen은 스와이프해서 내리면 뒷부분이 검정색이 떠요; // )
자! 그럼
준비가 끝났으니 시작해봅시다!
🚀 만약, 스토리보드를 사용한다면!
1️⃣ 손으로 dismiss해 줄 화면에 command + shift + L 을 눌러서 'Pan Gesture Recognizer'를 추가해주세요!
그러면 scene 상단에 파란색으로 메뉴가 하나 추가됐을 거에요.
그게 바로 Pan Gesture Recognizer 입니다!
2️⃣ 스토리보드에서 Ctrl을 누르고 이 Pan Gesture Recognizer로 끌어주면
3️⃣ 이렇게 뜰 거에요! 그러면 선택해줍니다!
선택이 완료됐다면! 우리는 우리 뷰에 Pan Gesture Recognizer를 추가완료해준 것입니다!
아촤촤!! 근데 저는 이번에는 코드로 해볼라구요,,
(풀코드는 맨 아래에 있습니다.)
🚀 코드로 한다면
공식 문서에 나와 있는 것과 같이 addGestureRecognizer(_:) 메소드를 사용해주면 됩니다.
원하는 뷰에 gesture recognizer를 연결해주는 메소드로
스토리보드에서 오브젝트 라이브러리에서 뷰에 Pan gesture Recognizer를 끌어다 놓는 것을 의미해요.
1️⃣ 생명주기 함수 (viewDidLoad 함수 아래에 새로운 함수를 만들어주세요!)
저는 modalDismiss라는 이름으로 해줬습니다.
이 함수 안에! 우리의 view에 PanGestureRecognizer를 추가해주는 코드를 작성해주고
그 함수를 viewDidLoad()에 불러주면,,! 잘 실행되겠죠?!
아래 코드는 그런 의미입니다!
자,, 그럼 자세한 코드 설명을 써볼게요~
view.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(handleDismiss)))
: 여기서 view는 우리의 Superview를 말합니다.
이 최상위 뷰에 GestureRecognizer를 추가해주는데,,,,
(target: self, action: #selector(handleDismiss)) 가 어떤 의미냐면,,,
: 우리의 target인 self 즉, 여기서는 [ MyProfileViewController ] 가
[ handleDismiss ] 라는 함수를 수행해야 함을 의미합니다.
**
✅ #selector는 Objective - C에서 사용된 클래스 메소드 이름을 가리키는 것이고,,
근데 이게 스위프트로 넘어오면서 함수를 직접 지정하는 기능을 가진 일종의 함수 선택자라고 해요..
(사실.. 음,,, 제가 이해하기론 "이 함수를 실행해라!" 이거 같은데 맞는지 잘 모르겠습니다..; 전 아직.. 초보니까여^^
아시는 분,, 친절한 댓글에서의 설명은,, 아요초보에게 큰 힘이 됩니다.. 허헣..)
**
자,, 그러면 handleDismiss 라는 함수를 불러올 거니까 우리가 새로 만들어주면 되겠죠?
아촤촤,,,,,!🤯
2️⃣ 그 전에 전역변수를 하나 만들어줄 겁니다.
왜냐하면 view가 움직인 위치를 저장하고, view의 움직인 방향을 저장해주기 위해서죠!
그래서 저는 생명주기 함수 바로 위에!
viewTranslation 과 viewVelocity 프로퍼티를 각각 CGPoint(x: 0, y: 0)으로 초기화해주었습니다.
-> viewTranslation에는 translation 메소드를 통해 위치 좌표값을 반환하고
-> viewVelocity에는 velocity 메소드를 통해 초당 방향의 좌표값을 반환받아 저장하려고 합니다.
3️⃣ 이제 handleDismiss 함수를 만들어 주면 됩니다.
앞에 @objc 가 붙는 이유는 #selector가 objective-c 문법이기 때문이라네요.
handleDismiss 함수 내부에 이제 실제로 우리가 스와이프할 때 각각의 조건을 설정해주면 되는데요.
먼저 앞서 말했다시피 요롷게 값을 저장해줍시다..!
viewTranslation = sender.translation(in: view) => view가 이동한 위치를 저장
viewVelocity = sender.velocity(in: view) => view가 이동한 방향을 저장
그리고 switch ~ case 구문을 통해
손가락으로 변화를 주는 .changed 상태와
손가락을 뗀 .ended 상태에 따른 코드를 작성해 줄 거에요.
4️⃣ .changed 상태를 먼저 봅시다.
우리는 무조건 아래로만 스와이프가 가능하게 해줘야 해요!
if viewVelocity.y > 0 {
: y값이 0보다 클 때 즉,,,
스와이프를 아래로만 할 경우에만 조건을 실행시킨다는 뜻입니다.
왜..? y가 0보다 큰데,, 아래로 스와이프냐구요...? 그건 이 링크를 참고해주세요
이렇게 if문을 통해서 조건을 설정해주지 않을 경우에는 위아래로 스와이프가 되는 불상사가 발생..해버림..ㅠ
UIView.animate(withDuration: 0.1, animations: {
~
})
: 위 코드는 animate 함수를 통해서 0.1초간 애니메이션을 줄 거고 (withDuration = 애니메이션 동작 시간)
animation: { ~ } 은 애니메이션이 동작하는 구간을 의미합니다. 📚 참고링크
왜,,, 0.1초를 줬냐..? 사실.. 다양한 시간을 줘봤는데 0.1초가 제일 매끄럽더라구여,,^^
self.view.transform = CGAffineTransform(translationX: 0, y: self.viewTranslation.y)
: 여기서 transform이 뭐나면,, 바로 transform은 CGAffineTransform이라는 타입인 프로퍼티인데요.
이 프로퍼티를 통해 뷰의 사이즈, 회전 등을 할 수 있다고 해여
그 중에서도 우리는! 뷰의 위치를 이동시킬 거에요!
transform을 통해 x, y 좌표를 변경해서 이동할 수 있는데
예를 들어,
transform = CGAffineTransform(translationX: 0, y: -100)
이건 x좌표는 그대로, y좌표는 -100이니 100만큼 위로 이동시킨다는 뜻이에여
그래서 (translationX:0, y:self.viewTranslation.y)
이렇게 써주는 것입니다.
: x 좌표는 변화가 없고, (어차피 위아래로만 움직이기 때문에,,)
y 좌표는 [ view의 y가 이동한 위치 ]만큼 이동시키자!는 것이죠.
5️⃣ 그 다음은 .ended 입니다.
우리가 카톡 프로필 창에서 손가락을 뗄 때, 두 가지 경우가 있어요..!
바로 화면을 어디까지 내리느냐에 따라서
1 > 모달 창이 내려지다 마는 경우
2 > 모달 창이 완전히 내려가는 경우
그렇기 때문에 if ~ else 를 써서 나눠줬는데요.
이번에는 y의 방향이 아니라 y의 위치를 기준으로 나눠야 해요.
저는 y 좌표가 400인 지점을 기준으로 줬어요.
if viewTranslation.y < 400 {
UIView.animate(withDuration: 0.1, animations: {
self.view.transform = .identity
})
: 400보다 작을 경우에는 모달창이 내려가지 않고 다시 올라가도록 하기 위해
transform의 기능 중 원상 복구가 되는 기능인 identity 키워드를 사용했어요!
self.view.transform = CGAffineTransform.identity
완전 간편하죠! 따로 0,0 좌표를 입력하지 않아도 된다는 점~~👍🏻👍🏻
} else {
dismiss(animated: true, completion: nil)
}
: 그리고 400이 넘는 좌표에서 손가락을 뗄 경우에는
모달창이 '아~ 사용자가 이 화면을 끄길 원하는 구나~'라고 인식하고 내려가도록 하기 위해
dismiss 메소드를 불러왔습니다~
** ✅ modal 은 present / dismiss 인 거 기억하져..?!?!?
그러면 끝입니다! yeah~!~!
switch ~ case 구문이니까
default 값으로 break 꼭 써주세요!
🚀✨☄️ 결과화면~!~!
후~~~~ 길고 기네요,,,🤯🤯
글고 참 많은 개념들이 들어 있어서 하나하나 이해하는데 시간이 걸렸네요,,
후하후하후하,,,
비록 구글링을 통해 구현했지만,, 제 지식으로 만들기 위해 이해하는 것은.. 쉽지 않네여,,
여전히 모르는 것들이 있지만,,, 흑,,ㅠㅠ
🧨 풀코드
* 스토리보드를 사용해서 @IBAction, @IBOutlet을 연결해줘야 합니둥..
넘어가기 전 뷰
import UIKit
class FriendViewController: UIViewController {
//MARK: - Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
//MARK: - Helpers
@IBAction func profileButton(_ sender: Any) {
guard let nextVC = self.storyboard?.instantiateViewController(identifier: "MyProfileViewController") as? MyProfileViewController else { return }
nextVC.modalPresentationStyle = .overFullScreen
self.present(nextVC, animated: true, completion: nil)
}
}
넘어간 후의 뷰 (아래로 스와이프할 뷰)
import UIKit
class MyProfileViewController: UIViewController {
//MARK: - Properties
var viewTranslation = CGPoint(x: 0, y: 0)
var viewVelocity = CGPoint(x: 0, y: 0)
//MARK: - Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
modalDismiss()
}
//MARK: - Helpers
func modalDismiss() {
view.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(handleDismiss)))
}
@objc func handleDismiss(_ sender: UIPanGestureRecognizer) {
viewTranslation = sender.translation(in: view)
viewVelocity = sender.velocity(in: view)
switch sender.state {
case .changed:
// 상하로 스와이프 할 때 위로 스와이프가 안되게 해주기 위해서 조건 설정
if viewVelocity.y > 0 {
UIView.animate(withDuration: 0.1, animations: {
self.view.transform = CGAffineTransform(translationX: 0, y: self.viewTranslation.y)
})
}
case .ended:
// 해당 뷰의 y값이 400보다 작으면(작게 이동 시) 뷰의 위치를 다시 원상복구하겠다. = 즉, 다시 y=0인 지점으로 리셋
if viewTranslation.y < 400 {
UIView.animate(withDuration: 0.1, animations: {
self.view.transform = .identity
})
// 뷰의 값이 400 이상이면 해당 화면 dismiss
} else {
dismiss(animated: true, completion: nil)
}
default:
break
}
}
@IBAction func closeButton(_ sender: Any) {
self.dismiss(animated: true, completion: nil)
}
}
'⭐️ 개발 > iOS & Swift' 카테고리의 다른 글
[iOS] 데이터 직접 전달 방식(1) - Property를 통해 전달 (1) | 2021.04.29 |
---|---|
[Swift] 클래스(Class) /구조체(Struct) /열거형(Enum) 정리 + 차이점 (2) | 2021.04.29 |
[iOS] velocity.y < 0 이면 왜 방향이 up일까? (0) | 2021.04.18 |
[iOS] 스토리보드/코드로 화면 전환하기 - Navigation(push/pop) (1) | 2021.04.15 |
[iOS] Storyboard Reference 쓰는 이유와 방법 (0) | 2021.04.15 |