[SwiftUI] Naver Map API 연동 (오류 해결)

Boogios·2023년 7월 25일
1

[SwiftUI]

목록 보기
5/7
post-thumbnail

시작하기에 앞서...

Naver Map API를 SwiftUI에서 사용하기 위해 구글링을 많이 시도해봤다!
하지만, 생각보다 찾다보니 내가 원하던 기능들을 모두 한번에 정리해둔 글들이 없고 순서대로 정리되어 있지 않은 느낌이라 한번 정리해둬야겠다고 생각했다-!
SwiftUI를 사용하는 사람이 점점 많아지고 있고 도움이 조금이나마 됐으면 좋겠다!

참고: https://jeong9216.tistory.com/198 (AppDelegate 생략, Cocoapod 설치까지)

위 사이트에서 네이버 클라우드 사이트(https://www.ncloud.com)에 접속해서 클라이언트 ID를 받고 CocoaPod을 설치하는 법까지 정리가 잘 되어 있고 이 부분까지는 정리가 잘되있는 글이 많아서 따로 정리하지 않도록 하겠당-!

Info.plist 권한 추가

이 부분.. 구글링과 내용이 너무 달랐다.

Info.plist에 추가해야 하는 Key들

  • Privacy — Location Always and When In Use Usage Description
  • Privacy - Location When In Use Usage Description

구글링의 내용은 다 위의 key들중 하나만 등록해도 된다. 보통 Privacy — Location Always and When In Use Usage Description를 많이 추가해서 쓴다해서 처음엔 나도 이거 하나만 추가해두고 코드를 작성하기 시작했다..

하지만... Info.plist 오류 발생

The app's Info.plist must contain an “NSLocationWhenInUseUsageDescription” key with a string value explaining to the user how the app uses this data

네이버 맵 API 연동을 분명 잘 따라했는데 이런 오류를 발견했다...
분명 나는 Info.plist에 추가를 잘 해줬는데 왜 안되지? 하다가 혹시 몰라서 Privacy - Location When In Use Usage Description를 추가하니 너무 허무하게 해결이 되었다...

그래서 다들 처음부터 2개를 다 추가해주고 사용하는걸 추천한다..!! (맞는진 모르겠다...)

위 사진과 같이 Info.plist를 설정해주면 일단 초기 설정은 끝이다!

Info.plist에 쓰는 value 값은 위와 같이 설명에 그대로 들어가니 상세하게 적어야 사용자도 이해하기 쉽고 앱 심사할 때도 리젝당하지 않으니 귀찮다고 막쓰면 안된다!!

MapView

struct MapView: View {
    var body: some View {
        VStack {
            NaverMap()
                .ignoresSafeArea(.all, edges: .top)
        }
    }
}

우선, MapView에 NaverMap을 서브뷰로 넣어서 불러오려고 한다!

네이버 맵은 아직까지 UIKit만 지원을 해주고 있어서 SwiftUI에서는 UIViewRepresentable을 사용해야 한다.

struct NaverMap: UIViewRepresentable {
    
    func makeCoordinator() -> Coordinator {
        Coordinator.shared
    }
    
    func makeUIView(context: Context) -> NMFNaverMapView {
        context.coordinator.getNaverMapView()
    }
    
    func updateUIView(_ uiView: NMFNaverMapView, context: Context) {}
    
}

Coordinator는 SwiftUI-UIKit간의 브릿지 역할을 하는 녀석이라고 생각하면 된다!

간단하게 요약하자면,
@Binding property는 SwiftUI -> UIKit 으로의 변수 전달이고,
Coordinator의 경우 UIKit -> SwiftUI로의 데이터 전달이라고 생각하면 쉽다-!

Coordinator라고 해서 새로운 개념 같지만, 사실상 "delegate"의 역할을 한다고 봐도 무방하다.

Naver Map에 필요한 Delegate를 사용하기 위해 Coordinator를 사용하려고 한다!

makeCoordinator() 함수는 말그대로 Cooridnator를 만드는 함수고,
코디네이터 클래스는 UIView -> SwiftUI로의 브릿지 역할을 하는 delegate라고 보면 된다.

Coordinator 클래스 생성

final class Coordinator: NSObject, ObservableObject, NMFMapViewCameraDelegate, NMFMapViewTouchDelegate, CLLocationManagerDelegate {
    static let shared = Coordinator()
    
    let view = NMFNaverMapView(frame: .zero)

NMFMapViewCameraDelegate 카메라 이동에 필요한 델리게이트,
NMFMapViewTouchDelegate 맵 터치할 때 필요한 델리게이트,
CLLocationManagerDelegate 위치 관련해서 필요한 델리게이트
들을 추가해주는 Coordinator 클래스를 생성한다.

init() 함수

// Coordinator 클래스 안의 코드

	override init() {
        super.init()
        
        view.mapView.positionMode = .direction
        view.mapView.isNightModeEnabled = true
        
        view.mapView.zoomLevel = 15
        view.mapView.minZoomLevel = 10 // 최소 줌 레벨
        view.mapView.maxZoomLevel = 17 // 최대 줌 레벨
        
        view.showLocationButton = true
        view.showZoomControls = true // 줌 확대, 축소 버튼 활성화
        view.showCompass = false
        view.showScaleBar = false
        
        view.mapView.addCameraDelegate(delegate: self)
        view.mapView.touchDelegate = self
    }

NMFNaverMapView()에서 기본적으로 제공해주는 기능들을 init()에서 처리해준다!

https://navermaps.github.io/ios-map-sdk/guide-ko/0.html
네이버에서 제공해주는 공식 문서를 한번 쭉 읽어보면 코드를 이해하기 쉬울거에요.. 꼭 읽어보시는거 추천!!!

https://navermaps.github.io/ios-map-sdk/guide-ko/4-1.html - 사용자 인터페이스
위에 링크 보시면 showLocationButton, showZoomControls, showCompass, showScaleBar 이런것들이 뭔지 이해가 바로 갈거에요! 저희가 항상 쓰던 익숙한 UI들입니다

카메라 이동 전, 후 콜백 함수

NMFMapViewCameraDelegate 프로토콜에서 사용할 수 있는 함수들이 있다.
카메라 이동 전에 호출되는 함수, 이동 후에 호출되는 함수, 다양하게 존재한다.

// Coordinator 클래스 안의 코드
	func mapView(_ mapView: NMFMapView, cameraWillChangeByReason reason: Int, animated: Bool) {
        // 카메라 이동이 시작되기 전 호출되는 함수
    }
    
    func mapView(_ mapView: NMFMapView, cameraIsChangingByReason reason: Int) {
        // 카메라의 위치가 변경되면 호출되는 함수
    }

위의 함수들은 아직 사용을 안해봐서 사용해보고 따로 정리해두려고 한다! 정리하면 링크 달아놔야징!

위치 정보 제공 동의 함수

CLLocationManagerDelegate 프로토콜을 따르게 해준 후에, CLLocationManager 객체를 생성후에,
CLLocationManager.authorizationStatus 값을 switch-case 문으로 나눠서 처리한다.
맨 처음에는 .notDetermined 상태이기 때문에 requestWhenInUseAuthorization() 함수가 실행되며 위에서 봤던, 우리가 익숙한 위치 정보 제공 동의 얼럿이 뜨게 된다.

// Coordinator 클래스 안의 코드
// 클래스 상단에 변수 설정을 해줘야 한다.
   	@Published var coord: (Double, Double) = (0.0, 0.0)
    @Published var userLocation: (Double, Double) = (0.0, 0.0)
    
	var locationManager: CLLocationManager?

// MARK: - 위치 정보 동의 확인
    func checkLocationAuthorization() {
        guard let locationManager = locationManager else { return }
        
        switch locationManager.authorizationStatus {
        case .notDetermined:
            locationManager.requestWhenInUseAuthorization()
        case .restricted:
            print("위치 정보 접근이 제한되었습니다.")
        case .denied:
            print("위치 정보 접근을 거절했습니다. 설정에 가서 변경하세요.")
        case .authorizedAlways, .authorizedWhenInUse:
            print("Success")
            
            coord = (Double(locationManager.location?.coordinate.latitude ?? 0.0), Double(locationManager.location?.coordinate.longitude ?? 0.0))
            userLocation = (Double(locationManager.location?.coordinate.latitude ?? 0.0), Double(locationManager.location?.coordinate.longitude ?? 0.0))
            
            fetchUserLocation()
            
        @unknown default:
            break
        }
    }

.authorizedAlways(항상 허용), .authorizedWhenInUse(앱 사용중만 허용) 상태일 경우에 사용자의 위치를 바로 가져올 수 있도록 생성해놓은 변수에 값을 저장한다.

위치 정보 제공 동의를 체크하는 함수

이 함수는 네이버 맵을 불러오는 MapView에서 사용한다.

// Coordinator 클래스 안의 코드
	func checkIfLocationServiceIsEnabled() {
        DispatchQueue.global().async {
            if CLLocationManager.locationServicesEnabled() {
                DispatchQueue.main.async {
                    self.locationManager = CLLocationManager()
                    self.locationManager!.delegate = self
                    self.checkLocationAuthorization()
                }
            } else {
                print("Show an alert letting them know this is off and to go turn i on")
            }
        }
    }

MapView에서 .onAppear 에서 위치 정보 제공을 동의 했는지 확인하는 함수를 호출한다.

위치 정보 제공 동의 순서
1. MapView에서 .onAppear에서 checkIfLocationServiceIsEnabled() 호출
2. checkIfLocationServiceIsEnabled() 함수 안에서 locationServicesEnabled() 값이 true인지 체크
3. true일 경우(동의한 경우), checkLocationAuthorization() 호출
4. case .authorizedAlways, .authorizedWhenInUse: 일 경우에만 fetchUserLocation() 호출

위와 같은 순서로 함수들이 호출된다. 복잡하긴 하지만 이 순서를 제대로 이해하면 위치정보제공 동의는 끝난거다!

struct MapView: View {
    
    // Coordinator 클래스
    @StateObject var coordinator: Coordinator = Coordinator.shared
    
    var body: some View {
        VStack {
            NaverMap()
                .ignoresSafeArea(.all, edges: .top)
        }
        .onAppear {
            Coordinator.shared.checkIfLocationServiceIsEnabled()
        }
    }
}

fetchUserLocation() 함수 - 사용자 위치에 Overlay 띄우고 시작하자마자 사용자 위치에 카메라 고정하기

지도가 보일 때, 사용자가 위치 기반 동의를 했다면 사용자의 위치 기준으로 맵을 보여주고 싶었다.
구글링을 많이 해본 결과, 찾기가 매우 어려웠다... 네이버 맵 API 문서와 구글링의 합작품이라고 보면 된다!

1. 우선, CLLocationManager를 통해 현재 위치의 위도 경도 값을 가져온다.

2. NMFCameraUpdate 함수로 카메라(맵)을 현재 위치로 이동 시켜준다

NMFCameraUpdate는 맵을 이동켜주는 함수이고 NMFMapViewCameraDelegate 프로토콜을 받았기 때문에 사용 가능한 함수이다. animation을 지정해줄 수 있고, 줌 크기도 설정 가능하다.
적용하려고 하는 NMFNaverMapView에 moveCamera(cameraUpdate) 함수를 통해 이동시킨다.

3. NMFMapView에서 제공하는 locationOverlay를 현재 위치로 설정한다.

현재 사용자의 위치를 overlay로 설정해주기 위해 icon을 기본적으로 제공하고 있는 이미지로 정했다!

// Coordinator 클래스 안의 코드
	func fetchUserLocation() {
        if let locationManager = locationManager {
            let lat = locationManager.location?.coordinate.latitude
            let lng = locationManager.location?.coordinate.longitude
            let cameraUpdate = NMFCameraUpdate(scrollTo: NMGLatLng(lat: lat ?? 0.0, lng: lng ?? 0.0), zoomTo: 15)
            cameraUpdate.animation = .easeIn
            cameraUpdate.animationDuration = 1
            
            let locationOverlay = view.mapView.locationOverlay
            locationOverlay.location = NMGLatLng(lat: lat ?? 0.0, lng: lng ?? 0.0)
            locationOverlay.hidden = false
            
            locationOverlay.icon = NMFOverlayImage(name: "location_overlay_icon")
            locationOverlay.iconWidth = CGFloat(NMF_LOCATION_OVERLAY_SIZE_AUTO)
            locationOverlay.iconHeight = CGFloat(NMF_LOCATION_OVERLAY_SIZE_AUTO)
            locationOverlay.anchor = CGPoint(x: 0.5, y: 1)
            
            view.mapView.moveCamera(cameraUpdate)
        }
    }

getNaverMapView() 함수를 통해 만든 뷰 불러오기

// Coordinator 클래스 안의 코드
	func getNaverMapView() -> NMFNaverMapView {
        view
    }

위에서 설정해준 NaverMap에서 이 함수를 통해 만든 맵을 불러온다

struct NaverMap: UIViewRepresentable {
    
    func makeCoordinator() -> Coordinator {
        Coordinator.shared
    }
    
    func makeUIView(context: Context) -> NMFNaverMapView {
        context.coordinator.getNaverMapView()
    }
    
    func updateUIView(_ uiView: NMFNaverMapView, context: Context) {}
    
}

완성-!!

저기 위에 보이는 마커는 테스트용으로 일단 찍어봤다!
내 위치에서부터 맵이 보이고 다른곳으로 옮긴 후 왼쪽하단에 버튼을 누르면 내 위치로 다시 되돌아오는걸 확인할 수 있다!

마치며...

SwiftUI에서 네이버 맵 API를 활용하려고 하는 사람들이 많아지고 있고 처음 시작할 때, 구글링하면 나오겠지.. 라고 생각했던게 하루가 꼬박 걸렸다... 생각보다 정리가 잘 되어 있지 않아서 나도 최대한 순서대로 설명을 하고자 했는데 큰 도움이 됐으면 좋겠다-!

profile
iOS Developer

0개의 댓글