본문 바로가기

iOS/SwiftUI

[SwiftUI] Naver Map에 Custom Marker 띄우기

이번에는 SwiftUI Naver Map에 Custom Marker를 표시해주는 작업까지 해보겠습니다.

이를 구현하기 위해서 아래 순서대로 진행해보겠습니다.

 

  1. UIViewRepresentable을 이용하여 Naver Map 표시
  2. Custom Marker로 사용할 SwiftUI View 작업
  3. 작업한 SwiftUI View를 UIView로 변환
  4. 변환한 UIVIew를 캡쳐하여 UIImage 생성
  5. 해당 UIImage를 Marker 이미지로 지정

 

SwiftUI에서 Naver Map을 사용하려면 UIViewRepresentable을 이용해야 합니다.

이 내용은 지난 포스팅에서 다루었으니, 여기를 참고해주세요! -> https://k00kie-dev.tistory.com/5

 

[SwiftUI] SwiftUI로 Naver Map iOS SDK 연동하기

이번에는 SwiftUI로 Naver Map iOS SDK를 연동해서 지도를 띄워보겠습니다. 2022.02.11 기준 Naver Map 최신 버전은 UIKit으로 구현이 가능합니다. 즉, UIView로 구현된 지도를 SwiftUI View로 사용하려면 UIViewR..

k00kie-dev.tistory.com

 

 

Naver Map을 화면에 표시했다면, Custom Marker로 사용할 SwiftUI View가 있어야겠죠?

저는 검은 마커 이미지 위에 말풍선이 위치하고, 말풍선(AddressBubble) 안에 해당 장소의 이름과 주소를 표시할 수 있는 View를 구현했습니다.

struct InfoWindowView: View {

    var name: String
    var address: String?

    var body: some View {
        VStack(spacing: 0) {
            VStack(alignment: .leading, spacing: 0) {
                Text(name)
                    .font(.custom("NotoSansCJKkr-Bold", size: 13))
                    .foregroundColor(Color(red: 0.12156862745098039, green: 0.12156862745098039, blue: 0.12156862745098039))
                Text(address ?? "")
                    .font(.custom("NotoSansCJKkr-Regular", size: 13))
                    .foregroundColor(Color(red: 0.6588235294117647, green: 0.6588235294117647, blue: 0.6588235294117647))
                    .lineLimit(1)
            } //VStack
            .padding(.top, 10)
            .padding(.bottom, 17)
            .padding(.horizontal, 17)
            .background(
                AddressBubble()
            )
            .padding(.bottom, 5)

            Image("ic_position_marker")
        }
    }
}

/**
말풍선 뷰
*/
struct AddressBubble: View {
    var body: some View {
        ZStack(alignment: .bottom) {
            VStack(spacing: 0) {
                RoundedRectangle(cornerRadius: 12)
                    .stroke(lineWidth: 1)
                    .background(Color.white)
                    .cornerRadius(12)
                
                Spacer()
            }
            
            Triangle()
                .stroke(lineWidth: 1)
                .frame(width: 12, height: 8)
                .background(Color.white)
                .clipShape(Triangle())
            
            Rectangle()
                .frame(width: 10.5, height: 2)
                .foregroundColor(.white)
                .padding(.bottom, 7.5)
        }
    }
    
    struct Triangle: Shape {
        func path(in rect: CGRect) -> Path {
            var path = Path()
            
            path.move(to: CGPoint(x: rect.midX, y: rect.maxY))
            path.addLine(to: CGPoint(x: rect.minX, y: rect.minY))
            path.addLine(to: CGPoint(x: rect.maxX, y: rect.minY))
            path.addLine(to: CGPoint(x: rect.midX, y: rect.maxY))
            
            return path
        }
    }
}

다음은 이 View를 캡쳐하여 UIImage를 생성해야합니다.

Custom Marker를 표시하려면 기본 Marker를 대체할 이미지를 지정해주어야하기 때문이죠.

 

Marker에 대한 더 자세한 내용은 네이버 공식 문서를 확인해주세요! -> https://navermaps.github.io/ios-map-sdk/guide-ko/

 

 

SwiftUI View를 캡쳐하려면 먼저 UIView로 변환하고 그 UIVIew를 UIGraphicsImageRenderer를 이용해 캡쳐하여 UIImage를 생성합니다.

이 과정도 지난 포스팅에서 다루었으니, 여기를 확인해주세요! -> https://k00kie-dev.tistory.com/4

 

[SwiftUI] SwiftUI View 캡쳐하기 (UIGraphicsImageRenderer)

SwiftUI View를 캡쳐하는 기능이 필요할 때가 있습니다. 앱에 화면 캡쳐 기능을 제공하는 것이 대표적인 사례겠죠? 저는 Naver Map SDK를 사용하면서 marker 이미지로 제가 만든 SwiftUI View를 사용하기 위

k00kie-dev.tistory.com

 

 

UIImage를 생성하는데 성공하셨다면, 그 UIImage로 NMFOverlayImage를 생성하여 marker.iconImage에 넣어주면 됩니다.

다음은 UIHostingController를 이용하여 SwiftUI View를 UIView로 변환하고, 그 UIView를 캡쳐하여 marker 이미지로 지정해주는 코드입니다.

//마커
let marker = NMFMarker(position: data.getLatLng())

//마커 이미지 구성
let infoWindowView = InfoWindowView(name: data.name, address: address)
let controller = UIHostingController(rootView: infoWindowView)
controller.view.frame = CGRect(origin: .zero, size: CGSize(width: 240, height: 110))
controller.view.backgroundColor = .clear

if let rootVC = UIApplication.shared.windows.first?.rootViewController {
    rootVC.view.insertSubview(controller.view, at: 0)

    let renderer = UIGraphicsImageRenderer(size: CGSize(width: 240, height: 125))

    let infoWindowImage = renderer.image { context in
        controller.view.layer.render(in: context.cgContext)
    }

    marker.iconImage = NMFOverlayImage(image: infoWindowImage)
    controller.view.removeFromSuperview()
}

marker.mapView = view.mapView
marker.touchHandler = { _ in
    onMarkerTouched()
    return true
}

 

최종 코드는 다음과 같습니다.

import SwiftUI
import NMapsMap

struct CustomMapView: View {
    
    @Binding var isActive: Bool
    let data: MarkerData?
    let address: String?
    
    var body: some View {
        VStack(spacing: 0) {
            ZStack {
                HStack {
                    BackButton($isActive, isBlack: true) //뒤로가기 버튼 (커스텀 뷰입니다)
                    
                    Spacer()
                }
                
                HStack {
                    Spacer()
                    Text("네이버 지도")
                        .font(.custom("NotoSansCJKkr-Bold", size: 16))
                        .foregroundColor(Color(red: 0.12156862745098039, green: 0.12156862745098039, blue: 0.12156862745098039))
                    Spacer()
                }
            }
            .padding(.vertical, 17)
            .padding(.horizontal, 25)
            
            Rectangle()
                .frame(height: 1)
                .foregroundColor(Color(red: 0.9607843137254902, green: 0.9607843137254902, blue: 0.9607843137254902))
            
            CustomMapViewRepresentable(data: MarkerData, address: address) {
                //마커 탭 이벤트 핸들링
            }
        } //VStack
        .navigationBarHidden(true)
    }
}

struct CustomMapViewRepresentable: UIViewRepresentable {
    
    let data: MarkerData?
    let address: String?
    let onMarkerTouched: () -> Void
    
    init(data: MarkerData?, address: String?, onMarkerTouched: @escaping () -> Void) {
        self.data = MarkerData
        self.address = address
        self.onMarkerTouched = onMarkerTouched
    }
    
    func makeUIView(context: Context) -> NMFNaverMapView {
        let view = NMFNaverMapView()
        view.showCompass = true
        if LocationManager.sharedInstance.isAuthorized() {
            view.showLocationButton = true
        }
        view.mapView.positionMode = .normal
        view.mapView.zoomLevel = 10
        view.mapView.addCameraDelegate(delegate: context.coordinator)
        
        if let data = self.data {
            //카메라 위치
            view.mapView.moveCamera(
                NMFCameraUpdate(scrollTo: data.getLatLng())
            )
            
            //마커
            let marker = NMFMarker(position: data.getLatLng())
            
            //마커 이미지 구성
            let infoWindowView = InfoWindowView(name: data.name, address: address)
            let controller = UIHostingController(rootView: infoWindowView)
            controller.view.frame = CGRect(origin: .zero, size: CGSize(width: 240, height: 110))
            controller.view.backgroundColor = .clear
            
            if let rootVC = UIApplication.shared.windows.first?.rootViewController {
                rootVC.view.insertSubview(controller.view, at: 0)
                
                let renderer = UIGraphicsImageRenderer(size: CGSize(width: 240, height: 125))
                
                let infoWindowImage = renderer.image { context in
                    controller.view.layer.render(in: context.cgContext)
                }
                
                marker.iconImage = NMFOverlayImage(image: infoWindowImage)
                controller.view.removeFromSuperview()
            }
            
            marker.mapView = view.mapView
            marker.touchHandler = { _ in
                onMarkerTouched()
                return true
            }
        }
        
        return view
    }
    
    func updateUIView(_ uiView: NMFNaverMapView, context: Context) {
    }
    
    func makeCoordinator() -> Coordinator {
        return Coordinator()
    }
    
    class Coordinator: NSObject {
        
    }
}

extension CustomMapViewRepresentable {
    struct InfoWindowView: View {
        
        var name: String
        var address: String?
        
        var body: some View {
            VStack(spacing: 0) {
                VStack(alignment: .leading, spacing: 0) {
                    Text(name)
                        .font(.custom("NotoSansCJKkr-Bold", size: 13))
                        .foregroundColor(Color(red: 0.12156862745098039, green: 0.12156862745098039, blue: 0.12156862745098039))
                    Text(address ?? "")
                        .font(.custom("NotoSansCJKkr-Regular", size: 13))
                        .foregroundColor(Color(red: 0.6588235294117647, green: 0.6588235294117647, blue: 0.6588235294117647))
                        .lineLimit(1)
                } //VStack
                .padding(.top, 10)
                .padding(.bottom, 17)
                .padding(.horizontal, 17)
                .background(
                    AddressBubble()
                )
                .padding(.bottom, 5)
                
                Image("ic_position_marker")
            }
        }
    }
}

Custom Marker 결과 화면