출시 프로젝트에서 사용한 네이버 지도는 기본적인 제스처가 설정돼있다.
일반적으로 Pan 제스처는 카메라를 상하좌우로 이동시키고, Pinch 제스처는 카메라의 확대 축소를 담당한다.
그런데 사용자 주변의 특정 장소를 찾는다는 앱의 특성 상 사용자의 위치를 중심으로 지도의 카메라가
움직이는 새로운 조작법을 만들고 싶었다.
이 글은 게임 포켓몬고의 제스쳐 액션을 UIGestureRecognizer를 통해 구현한 경험을 정리한 글이다.
1. 분석
포켓몬고의 메인 화면에서 Pan
제스처는 카메라의 회전을 담당한다.
캐릭터를 중심으로 원을 그리면서 Panning하면 해당 방향(시계 또는 반시계)으로 카메라가 회전한다.
이 제스처로 사용자 위치 주변을 360도로 둘러볼 수 있다.
또한 포켓몬고의 Pinch
제스처는 카메라의 줌과 기울기(틸트)를 동시에 조절한다.
손가락을 확대하면 카메라가 낮아지면서 내 캐릭터에 가까이 다가가고
손가락을 축소하면 카메라가 높아지면서 캐릭터로부터 멀어지므로
내 주변의 시야를 넓히거나 좁힐 수 있다.
2. 구현
1) UIPanGestureRecognizer
Pan 제스처를 카메라의 회전으로 바꾸기 위해서 UIPanGestureRecognizer
의
translation(in:)
과 location(in:)
메서드를 활용할 것이다.
UIPanGestureRecognizer
는 Pan 제스처를 인식할 때마다 Target에 있는
지정된 액션 메서드를 호출하며 translation
과 location
이라는 특정 값을 보고한다.
UIPanGestureRecognizer의 메서드를 사용해서 보고되는 값들을 확인할 수 있는데,
이 값들은 특정 뷰의 좌표계 안에서 발생한 제스처가 CGPoint 타입으로 번역된(interpret) 것이다.
location(in:)
은 좌표계 평면에서 Pan 제스처가 발생한 위치 좌표를 리턴하고
translation(in:)
은 처음 Pan 제스처가 시작된 위치와 현재 제스처가 발생한 위치와의 변위차를 리턴한다.
따라서 UIPanGestureRecognizer
의 액션 메서드가 호출될 때마다 바로 직전의 translation
과
현재 translation
을 비교해서 x축, y축으로 얼마만큼 움직였는지 그 변화량을 계산할 수 있다.
유의할 점은 translation
값은 처음 제스처가 발생한 위치를 기준으로 하기 때문에
매번 액션 메서드 호출 시 마다 translation
을 (0,0)으로 초기화 해주어야 한다.
초기화 해준 뒤 바로 다음에 보고받는 translation
값이 바로 이전 위치와 현재 위치 사이의 변화량이다.
이렇게 구한 위치 변화량을 카메라가 회전하는 정도(각)로 변경해주어야 한다.
만약 위 그림처럼 카메라가 뷰의 중심을 기준으로 회전한다고 가정했을때
v1은 시작점이 뷰의 중심이고 끝점이 현재 제스처 위치를 가리키는 벡터이고
v2는 v1에 위치 변화량(translation)
을 더한 벡터이다.
이때 각 변화량(θ)은 두 벡터 v1과 v2 사이의 각과 같다.
이 변화량은 단위가 라디안이므로 180/Pi를 곱해서 각도를 구해주면 된다.
@objc private func rotateHandler(_ sender: UIPanGestureRecognizer) {
let translation = sender.translation(in: mapView)
let location = sender.location(in: mapView)
if sender.state == .began {
} else if sender.state == .changed {
// rotating map camera
let bounds = mapView.bounds
let vector1 = CGVector(dx: location.x - bounds.midX, dy: location.y - bounds.midY)
let vector2 = CGVector(dx: vector1.dx + translation.x, dy: vector1.dy + translation.y)
let angle1 = atan2(vector1.dx, vector1.dy)
let angle2 = atan2(vector2.dx, vector2.dy)
let delta = (angle2 - angle1) * 180.0 / Double.pi
let param = NMFCameraUpdateParams()
param.rotate(by: delta)
let update = NMFCameraUpdate(params: param)
mapView.moveCamera(update)
} else if sender.state == .ended {
}
sender.setTranslation(.zero, in: mapView)
}
2) UIPinchGestureRecognizer
UIPinchGestrueRecognizer
는 화면에 터치한 두 손가락이 멀어지거나 가까워 질 때마다
scale
이라는 CGFloat
타입의 값을 연속적으로 Target에게 보고한다.
scale 프로퍼티는 제스처가 처음 시작됐을 때보다 두 손가락의 거리가 얼마나 달라졌는가를 실수로 나타낸다.
이 값으로 거리 변화량을 계산하여 카메라의 줌과 기울기 값에 적용할 수 있다.
제스처가 처음 시작될 때 scale
값이 1.0
이므로 액션 메서드를 통해 값이 보고될 때마다
scale
값을 1.0
으로 초기화해주면 바로 직전 제스처와 현재 제스처 사이의 거리 변화량을 구할 수 있다.
위 그림에서 현재 scale
값이 직전보다 0.2만큼 증가했으므로 두 손가락의 거리 변화량은 0.2이다.
이 변화량을 카메라의 줌 값에 더해주면 Pinch 제스처에 따라 카메라를 선형적으로 확대 또는 축소할 수 있다.
하지만 카메라의 줌과 기울기는 가질 수 있는 값의 범위가 다르기 때문에
이 변화량을 그대로 기울기에 더할 경우 기울기가 거의 변하지 않는 것 처럼 보인다.
즉, 줌은 값 범위가 작기 때문에 0.2 라는 값을 더한 것으로도 큰 변화가 발생하는 반면에
기울기는 값 범위가 커서 0.2를 값 범위에 맞는 다른 수치로 변경해주어야 한다.
따라서 [최소 줌, 최대 줌] 범위에서의 거리 변화량이 [최소 기울기, 최대 기울기] 에서
어떤 값을 갖는지 계산하고 기울기 값에 더해주면 된다.
@objc private func zoomAndTiltHandler(_ sender: UIPinchGestureRecognizer) {
let zoom = currentZoom
let tilt = currentTilt
let minZoom = mapView.minZoomLevel
let maxZoom = mapView.maxZoomLevel
if sender.state == .began {
} else if sender.state == .changed {
let deltaZoom = sender.scale-1
let deltaTilt = (maxTilt - minTilt) * deltaZoom / (maxZoom-minZoom)
let param = NMFCameraUpdateParams()
param.zoom(by: deltaZoom)
param.tilt(by: deltaTilt)
let update = NMFCameraUpdate(params: param)
mapView.moveCamera(update)
} else if sender.state == .ended {
}
sender.scale = 1
}
'IOS' 카테고리의 다른 글
[IOS] Realm 사용해서 데이터 저장하기 (0) | 2022.10.05 |
---|---|
[IOS] API 통신에서 겪은 SSL 인증서 및 ATS 관련 이슈에 대하여 (0) | 2022.10.05 |
[IOS] NaverMap SDK 사용 중 알게된 Git LFS에 대하여 (0) | 2022.10.03 |
[IOS] CocoaPod의 pod install 오류에 대하여 (0) | 2022.09.09 |
[IOS] UIView의 tintColor 프로퍼티에 대하여 (0) | 2022.07.27 |