[IOS] UIGestureRecognizer 사용해서 지도 조작하기

2022. 10. 19. 04:10·IOS

출시 프로젝트에서 사용한 네이버 지도는 기본적인 제스처가 설정돼있다.

일반적으로 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 제스처가 시작된 위치와 현재 제스처가 발생한 위치와의 변위차를 리턴한다.

 

location과 translation

 

따라서 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] WKWebView에서 웹문서 스크롤 높이 구하기  (2) 2025.07.04
Django 서버에서 iOS Push Notification 보내기  (0) 2025.06.26
[IOS] Realm 사용해서 데이터 저장하기  (0) 2022.10.05
[IOS] API 통신에서 겪은 SSL 인증서 및 ATS 관련 이슈에 대하여  (0) 2022.10.05
[IOS] NaverMap SDK 사용 중 알게된 Git LFS에 대하여  (0) 2022.10.03
'IOS' 카테고리의 다른 글
  • [iOS] WKWebView에서 웹문서 스크롤 높이 구하기
  • Django 서버에서 iOS Push Notification 보내기
  • [IOS] Realm 사용해서 데이터 저장하기
  • [IOS] API 통신에서 겪은 SSL 인증서 및 ATS 관련 이슈에 대하여
무슈후슈
무슈후슈
코딩은 창작이다.
  • 무슈후슈
    감성코드
    무슈후슈
  • 전체
    오늘
    어제
    • 분류 전체보기 (123) N
      • 알고리즘 (30)
      • IOS (28) N
      • Swift (4)
      • TIL (41)
      • CS (15)
      • 메모 (2)
      • 시플 (1)
      • RxSwift (2)
  • 블로그 메뉴

    • 홈
    • 태그
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    ios
    비동기
    파이썬
    codable
    http
    백준
    그래프 탐색
    프로그래머스
    git
    SWIFT
    MVVM
    Realm
    풀이
    이분 탐색
    다이나믹 프로그래밍
    알고리즘
    코딩테스트
    github
    그리디
    python
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
무슈후슈
[IOS] UIGestureRecognizer 사용해서 지도 조작하기
상단으로

티스토리툴바