본문 바로가기
iOS/UI

[Swift] iOS 네이버 지도 SDK - 마커 커스텀

by 바등쪼 2023. 7. 12.

최근에 진행한 프로젝트에서 네이버 지도 SDK를 사용하여 기능을 구현해야 했었습니다.

사용자가 직접 지도를 터치하여 달리기 코스를 그리는 것이 주요 요구사항이었습니다.

구현해야 하는 디자인은 다음과 같습니다.

 

디자인 요구사항 예시

 

요구사항

  1. 처음 입력받은 좌표는 출발지로 설정해야 한다. ➡️ 출발이라는 말풍선 이미지를 포함해야 하고 다른 마커들과 UI가 다르다.
  2. 특정 뷰에서는 출발지 마커에 말풍선이 없어져야 한다.
  3. 그 이후 사용자가 터치해서 생기는 마커들은 테두리가 있는 작은 원 모양이다.

위와 같이 크게 3개의 요구사항으로 정리할 수 있었습니다!

 

출발지 마커
경유지 마커

그렇다면 제가 구현해야 할 마커는 출발지 마커와 경유지 마커입니다.

 

구현

사용한 NMapsMap SDK 버전 : 3.16.1

네이버 지도 SDK는 다양한 기능을 제공하고 있고 마커 역시 제공합니다.

마커는 오버레이의 한 종류입니다.

다양한 마커 커스텀 옵션을 제공하고 있으니 링크를 참고해주세요!

 

네이버 지도 SDK에서는 NMFMarker 클래스로 마커를 제공합니다.

디자인 요구사항에 따라 마커는 여러 곳에서 사용됩니다. 따라서 재사용성을 위해 커스텀한 마커를 분리해서 구현할 필요가 있었습니다.

 

저는 상속을 활용하여 NMFMarker를 커스텀하였습니다.

NMFMarker의 기본 기능들을 가져오고 우리가 필요한 새로운 부분만 추가로 구현하기 때문에 상속이 적합했습니다.

 

RNStartMarker (출발지 마커)

출발지 마커의 이름을 프로젝트 이름인 Runnect에서 따와 RNStartMarker로 정했습니다.

import UIKit

import NMapsMap

final class RNStartMarker: NMFMarker {
    
    // MARK: - UI & Layout
    
    let startInfoWindow = NMFInfoWindow()
    
    // MARK: - initialization
    
    override init() {
        super.init()
        setUI()
        setInfoWindow()
    }
}

// MARK: - UI & Layout

extension RNStartMarker {
    private func setUI() {
        let image = NMFOverlayImage(image: ImageLiterals.icMapDeparture)
        self.iconImage = image
        
        self.width = CGFloat(NMF_MARKER_SIZE_AUTO)
        self.height = CGFloat(NMF_MARKER_SIZE_AUTO)
        
        self.anchor = CGPoint(x: 0.5, y: 0.5)
        
        self.iconPerspectiveEnabled = true
    }
    
    private func setInfoWindow() {
        startInfoWindow.dataSource = self
    }
    
    func showInfoWindow() {
        startInfoWindow.open(with: self)
    }
    
    func hideInfoWindow() {
        startInfoWindow.close()
    }
}

// MARK: - NMFOverlayImageDataSource

extension RNStartMarker: NMFOverlayImageDataSource {
    func view(with overlay: NMFOverlay) -> UIView {
        // 마커 위에 보여줄 InfoView 이미지 리턴
        let imageView = UIImageView(frame: CGRect(x: 0, y: 0, width: 58, height: 34))
        imageView.image = ImageLiterals.icMapStart
        return imageView
    }
}

출발지 마커에는 출발이라는 말풍선 이미지가 필요합니다.

SDK 차원에서 NMFInfoWindow 클래스를 제공하고 있습니다. 마커 위 또는 지도의 특정 지점에 말풍선 모양의 오버레이를 표시해주는 기능을 수행하는 객체입니다. startInfoWindow로 이름 붙인 프로퍼티에 NMFInfoWindow 객체를 생성했습니다.

 

마침 저희도 말풍선 모양이 필요해서 그대로 사용하려고 했으나 기본 제공 디자인이 조금 달라 NMFOverlayImageDataSource를 채택하고 이미지 에셋으로 말풍선의 UI를 교체했습니다. 

ImageLiterals에 다음과 같이 선언된 UIImage를 사용했습니다.

static var icMapStart: UIImage { .load(named: "ic_map_start") }

특정 뷰에서는 이 말풍선 모양의 마커가 사라져야 한다는 요구사항이 있었기 때문에 startInfoWindow를 껐다 켰다 하는 함수를 추가로 생성했습니다. showInfoWindow()와  hideInfoWindow과 그것입니다.

오버레이는 공통적으로 open과 close로 추가/제거 가능합니다.

open 시에 파라미터로 대상 마커를 넣어야 하는데 여기서는 자기 자신이기 때문에 self로 넣었습니다.

 

 

setUI 함수를 살펴보겠습니다.

마커의 이미지 또한 교체했습니다.

iconImage를 원하는 이미지로 교체하면 됩니다. (NMFMarker 클래스가 가지고 있는 프로퍼티입니다. 상속했기 때문에 사용 가능합니다.)

 

⭐️ 이때 주의할 점이 있습니다!!

이미지의 타입을 NMFOverlayImage로 지정해서 넣어야 하는데 이 객체는 비트맵을 다루므로 사용 시 메모리 관리에 유의해야 합니다.

따라서 여러 오버레이가 같은 이미지를 사용할 경우 NMFOverlayImage의 인스턴스를 1개만 만들어서 공유하여 사용해야 합니다.
아래와 같이 말이죠!

let image = NMFOverlayImage(name: "marker_icon")
marker1.iconImage = image
marker2.iconImage = image

 

하지만 제 코드를 보면 각 마커 마다 새로 NMFOverlayImage를 생성하여 적용하고 있는 모습을 볼 수 있는데

공식 문서를 보면 UIImage 객체로부터 NMFOverlayImage를 생성하면 해당 UIImage가 동일할 경우 인스턴스가 다르더라도 동일한 비트맵을 공유한다고 합니다.

따라서

// OK: marker3, 4가 다른 NMFOverlayImage 객체를 사용하지만 참조하는 리소스가 같으므로 비트맵도 공유
marker3.iconImage = NMFOverlayImage(image: bitmap)
marker4.iconImage = NMFOverlayImage(image: bitmap)

이 코드 처럼 NMFOverlayImage의 생성자가 UIImage를 받는다면 제가 구현한 것처럼 각 마커가 다른 NMFOverlayImage 객체를 사용해도 메모리 관리에 문제가 없습니다.

 

SDK 공식 문서 : 메모리 관리 부분을 참고해주세요

 

 

다시 setUI로 돌아가 보겠습니다.

anchor를 지정해주고 있습니다.

anchor는 아이콘 이미지에서 기준이 되는 지점입니다.

기본값은 (0.5, 1)인데 이대로 하니까 경로선(pathOverlay)와 마커 연결 부분이 어색해서 중앙부분으로 위치시키기 위해 (x: 0.5, y: 0.5)로 설정했습니다.

 

RNMarker

경유지 마커입니다. 출발지 마커와 동일한 방법으로 구현하고 UI만 다르게 설정했습니다.

import UIKit

import NMapsMap

final class RNMarker: NMFMarker {
    
    // MARK: - initialization
    
    override init() {
        super.init()
        setUI()
    }
}

// MARK: - UI & Layout

extension RNMarker {
    private func setUI() {
        let image = NMFOverlayImage(image: ImageLiterals.icMapPoint) // 비트맵 공유를 통한 메모리 관리
        self.iconImage = image
        
        self.width = CGFloat(NMF_MARKER_SIZE_AUTO)
        self.height = CGFloat(NMF_MARKER_SIZE_AUTO)
        
        self.anchor = CGPoint(x: 0.5, y: 0.5)
        
        self.iconPerspectiveEnabled = true
    }
}

마커 이미지만 디자이너분들이 만들어주신 에셋으로 바꾸고 사이즈와 anchor를 조정했습니다.

여기서 NMF_MARKER_SIZE_AUTO는 너비 또는 높이가 자동임을 나타내는 상수로 지도 SDK에 선언되어 있는 값입니다.

너비 또는 높이가 자동이면 아이콘 이미지 크기에 맞춰집니다.

 

iconPerspectiveEnabled는 원근 효과를 적용하는 속성입니다. (기본값은 false입니다.)

 

 

결과

구현 결과

출발지 마커와 경유지 마커를 사용하여 구현한 뷰입니다.

출발지는 고정이고 사용자가 터치를 하면 해당 지점에 경유지 마커가 생성되는 모습을 확인할 수 있습니다!

 

 

마무리

네이버 지도 SDK에서 제공하는 기능들과 상속을 활용하여 마커를 커스텀하였고 재사용성을 높였습니다.

다음 글에서는 지도 뷰 자체를 커스텀하는 과정을 정리해 보겠습니다!

 

 

프로젝트 레포지토리

https://github.com/Runnect/Runnect-iOS

 

GitHub - Runnect/Runnect-iOS: 아요.. 런넥트로 우주까지 달려보까?! 🚀🪐👩🏻‍🚀

아요.. 런넥트로 우주까지 달려보까?! 🚀🪐👩🏻‍🚀. Contribute to Runnect/Runnect-iOS development by creating an account on GitHub.

github.com

 

댓글