SwiftUI 카카오맵(v2) 그리기 - 맵 안보이는 문제 해결

Juhee Kim·2024년 9월 5일
0

iOS

목록 보기
2/4

0. 목표

  • SwiftUI를 사용하여 카카오맵 띄우기
  • 공식 가이드대로 쓴 코드의 오류 해결하기

1. 기존 코드

import SwiftUI
import KakaoMapsSDK

struct MapView: View {
    @State var draw: Bool = true   // 뷰의 appear 상태를 전달하기 위한 변수.
    
    init() {
        SDKInitializer.InitSDK(appKey: Config.kakaoAppKey)
    }
    
    var body: some View {
        KakaoMapView(draw: $draw)
            .onAppear(perform: {
                self.draw = true
            })
            .onDisappear(perform: {
                self.draw = false
            })
            .frame(maxWidth: .infinity, maxHeight: .infinity)
    }
}

struct KakaoMapView: UIViewRepresentable {
    @Binding var draw: Bool
    
    /// UIView를 상속한 KMViewContainer를 생성한다.
    /// 뷰 생성과 함께 KMControllerDelegate를 구현한 Coordinator를 생성하고, 엔진을 생성 및 초기화한다.
    func makeUIView(context: Self.Context) -> KMViewContainer {
        let view: KMViewContainer = KMViewContainer()
        view.sizeToFit()
        context.coordinator.createController(view)
        DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
            context.coordinator.controller?.prepareEngine()
            print("makeUIView - Engine prepared!")
        }
        
        return view
    }
    
    /// Updates the presented `UIView` (and coordinator) to the latest configuration.
    /// draw가 true로 설정되면 엔진을 시작하고 렌더링을 시작한다.
    /// draw가 false로 설정되면 렌더링을 멈추고 엔진을 stop한다.
    func updateUIView(_ uiView: KMViewContainer, context: Self.Context) {
        if draw {
            context.coordinator.controller?.activateEngine()
            print("updateUIView - Engine activated!")
        } else {
            context.coordinator.controller?.resetEngine()
        }
    }
    
    /// Coordinator 생성
    func makeCoordinator() -> KakaoMapCoordinator {
        return KakaoMapCoordinator()
    }
    
    /// Cleans up the presented `UIView` (and coordinator) in anticipation of their removal.
    static func dismantleUIView(_ uiView: KMViewContainer, coordinator: KakaoMapCoordinator) {
        
    }
    
    /// Coordinator 구현. KMControllerDelegate를 adopt한다.
    class KakaoMapCoordinator: NSObject, MapControllerDelegate {
        var controller: KMController?
        var first: Bool
        
        override init() {
            first = true
            super.init()
        }
        
        // KMController 객체 생성 및 event delegate 지정
        func createController(_ view: KMViewContainer) {
            controller = KMController(viewContainer: view)
            controller?.delegate = self
        }
        
        // KMControllerDelegate Protocol method구현
        
        // 엔진 생성 및 초기화 이후, 렌더링 준비가 완료되면 아래 addViews를 호출한다.
        // 원하는 뷰를 생성한다.
        func addViews() {
            let defaultPosition: MapPoint = MapPoint(longitude: 127.108678, latitude: 37.402001)
            let mapviewInfo: MapviewInfo = MapviewInfo(viewName: "mapview", viewInfoName: "map", defaultPosition: defaultPosition)
            
            controller?.addView(mapviewInfo)
        }
        
        // addView 성공 이벤트 delegate. 추가적으로 수행할 작업을 진행한다.
        func addViewSucceeded(_ viewName: String, viewInfoName: String) {
            print("OK") // 추가 성공. 성공시 추가적으로 수행할 작업을 진행한다.
        }
        
        // addView 실패 이벤트 delegate. 실패에 대한 오류 처리를 진행한다.
        func addViewFailed(_ viewName: String, viewInfoName: String) {
            print("Failed")
        }
        
        func authenticationSucceeded() {
            print("auth succeed!!")
        }
        
        func authenticationFailed(_ errorCode: Int, desc: String) {
            print("auth failed")
            print("error code: \(errorCode)")
            print(desc)
            //            print(controller?.getStateDescMessage())
        }
        
        // KMViewContainer 리사이징 될 때 호출.
        func containerDidResized(_ size: CGSize) {
            let mapView: KakaoMap? = controller?.getView("mapview") as? KakaoMap
            mapView?.viewRect = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: size)
            if first {
                let cameraUpdate: CameraUpdate = CameraUpdate.make(target: MapPoint(longitude: 127.108678, latitude: 37.402001), zoomLevel: 10, mapView: mapView!)
                mapView?.moveCamera(cameraUpdate)
                first = false
            }
        }
        
    }
}

2. 트러블 슈팅

이슈

  • 카카오맵 키 인증까지는 정상적으로 되지만 지도 뷰가 뜨지 않고 연한 초록색으로만 표시됨

원인

1️⃣

  • 뷰를 처음 그리는 경우 명시적으로 엔진을 활성화해야 함.
  • 공식 문서에 따르면 KakaoMapsSDK의 라이프 사이클은 엔진 초기상태 → 인증 시작 → 엔진 대기인증 성공엔진 활성지도 그리기 순으로 진행됨.
  • 인증 성공 후 직접 엔진 활성 코드를 호출하지 않았기 때문에 다음 작업인 지도 그리기가 진행되지 않았던 것.

2️⃣

  • 엔진 대기, 인증하는 과정에서 SDK 라이프사이클이 꼬임

🌟 해결

  • 인증이 완료되면 명시적으로 엔진을 활성화 함.
  • 또한 엔진 대기 부분을 비동기적으로 처리해 라이프사이클 문제를 해결함.

3. 최종 코드

기존 코드에서 달라진 부분은 🌟로 표시해두었다.

import SwiftUI
import KakaoMapsSDK

struct MapView: View {
    @State var draw: Bool = false // 뷰의 appear 상태를 전달하기 위한 변수.
    
    // 🌟 SDKInitializer는 AppDelegate에서 이미 설정했으므로 중복된 기능 제외
    
    var body: some View {
        KakaoMapView(draw: $draw)
            .onAppear(perform: {
                self.draw = true
            })
            .onDisappear(perform: { self.draw = false   })
            .frame(maxWidth: .infinity, maxHeight: .infinity)
    }
}

struct KakaoMapView: UIViewRepresentable {
    @Binding var draw: Bool
    
    /// UIView를 상속한 KMViewContainer를 생성한다.
    /// 뷰 생성과 함께 KMControllerDelegate를 구현한 Coordinator를 생성하고, 엔진을 생성 및 초기화한다.
    func makeUIView(context: Self.Context) -> KMViewContainer {
        let view: KMViewContainer = KMViewContainer()
        view.sizeToFit()
        context.coordinator.createController(view)
        // 🌟 prepareEngine 비동기적 처리하지 않으면 맵 보이지 않음. 
        // 🌟 DispatchQueue.main.asyncAfter(deadline: .now() + 2) 사용해도 되지만 대기시간이 있어 async()보다 뷰가 느리게 뜸
        DispatchQueue.main.async() {
            context.coordinator.controller?.prepareEngine()
            print("makeUIView - Engine prepared!")
        }
        
        return view
    }
    
    /// Updates the presented `UIView` (and coordinator) to the latest configuration.
    /// draw가 true로 설정되면 엔진을 시작하고 렌더링을 시작한다.
    /// draw가 false로 설정되면 렌더링을 멈추고 엔진을 stop한다.
    func updateUIView(_ uiView: KMViewContainer, context: Self.Context) {
        if draw {
            context.coordinator.controller?.activateEngine()
            print("updateUIView - Engine activated!")
        } else {
            context.coordinator.controller?.resetEngine()
        }
    }
    
    /// Coordinator 생성
    func makeCoordinator() -> KakaoMapCoordinator {
        return KakaoMapCoordinator()
    }
    
    /// Cleans up the presented `UIView` (and coordinator) in anticipation of their removal.
    static func dismantleUIView(_ uiView: KMViewContainer, coordinator: KakaoMapCoordinator) {
        
    }
    
    /// Coordinator 구현. KMControllerDelegate를 adopt한다.
    class KakaoMapCoordinator: NSObject, MapControllerDelegate {
        var controller: KMController?
        var first: Bool
        
        override init() {
            first = true
            super.init()
        }
        
        // KMController 객체 생성 및 event delegate 지정
        func createController(_ view: KMViewContainer) {
            controller = KMController(viewContainer: view)
            controller?.delegate = self
        }
        
        // KMControllerDelegate Protocol method구현
        
        // 엔진 생성 및 초기화 이후, 렌더링 준비가 완료되면 아래 addViews를 호출한다.
        // 원하는 뷰를 생성한다.
        func addViews() {
            let defaultPosition: MapPoint = MapPoint(longitude: 127.108678, latitude: 37.402001)
            let mapviewInfo: MapviewInfo = MapviewInfo(viewName: "mapview", viewInfoName: "map", defaultPosition: defaultPosition)
            
            controller?.addView(mapviewInfo)
        }
        
        // addView 성공 이벤트 delegate. 추가적으로 수행할 작업을 진행한다.
        func addViewSucceeded(_ viewName: String, viewInfoName: String) {
            print("OK") // 추가 성공. 성공시 추가적으로 수행할 작업을 진행한다.
        }
        
        // addView 실패 이벤트 delegate. 실패에 대한 오류 처리를 진행한다.
        func addViewFailed(_ viewName: String, viewInfoName: String) {
            print("Failed to add view")
        }
        
        func authenticationSucceeded() {
            print("auth succeed!!")
            print(controller?.isEnginePrepared)
            print(controller?.isEngineActive)
            // 🌟 인증이 완료되면 엔진을 활성화시킨다.
            if let controller = controller,
               !controller.isEngineActive {
                controller.activateEngine()
            }
        }
        
        func authenticationFailed(_ errorCode: Int, desc: String) {
            print("auth failed")
            print("error code: \(errorCode)")
            print(desc)
        }
        
        // KMViewContainer 리사이징 될 때 호출.
        func containerDidResized(_ size: CGSize) {
            let mapView: KakaoMap? = controller?.getView("mapview") as? KakaoMap
            mapView?.viewRect = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: size)
            if first {
                let cameraUpdate: CameraUpdate = CameraUpdate.make(target: MapPoint(longitude: 127.108678, latitude: 37.402001), zoomLevel: 10, mapView: mapView!)
                mapView?.moveCamera(cameraUpdate)
                first = false
            }
        }
        
    }
}

#Preview {
    MapView()
}

며칠을 고생하다가 드디어 지도 안보이는 이슈를 해결했다.

KakaoMapsSDK가 최근에 V2로 바뀜 + 계속 업데이트 되는 중 이슈로 생각보다 자료가 적었는데 해결되어 다행이다.👍

profile
개: 개롭지만 발: 발전하는중

0개의 댓글

Powered by GraphCDN, the GraphQL CDN