지도 그리기
KakaoMapsSDK으로 지도 그리기.
KakaoMapsSDK를 사용해서 지도를 그리기 위해 view를 구성하고 사용하는 대표적인 두 가지 방법(Storyboard, SwiftUI)에 대해 알아봅니다.
지도 그리기
Storyboard
KakaoMapsSDK를 통해서 지도를 표시하려면 KMViewContainer와 KMController를 사용해야 합니다. KMViewContainer는 UIView를 상속하므로 코드 혹은 Storyboard를 통해 생성해서 사용할 수 있습니다. 아래 예시는 KMController를 생성하여 KakaoMap을 지도에 표시하고, 앱의 뷰 상태에 따라 엔진을 컨트롤하는 예시입니다.
class APISampleBaseViewController: UIViewController, MapControllerDelegate {
required init?(coder aDecoder: NSCoder) {
_observerAdded = false
_auth = false
_appear = false
super.init(coder: aDecoder)
}
deinit {
mapController?.pauseEngine()
mapController?.resetEngine()
print("deinit")
}
override func viewDidLoad() {
super.viewDidLoad()
mapContainer = self.view as? KMViewContainer
//KMController 생성.
mapController = KMController(viewContainer: mapContainer!)!
mapController!.delegate = self
mapController?.prepareEngine() //엔진 초기화. 엔진 내부 객체 생성 및 초기화가 진행된다.
}
override func viewWillAppear(_ animated: Bool) {
addObservers()
_appear = true
if mapController?.isEngineActive == false {
mapController?.activateEngine()
}
}
override func viewDidAppear(_ animated: Bool) {
}
override func viewWillDisappear(_ animated: Bool) {
_appear = false
mapController?.pauseEngine() //렌더링 중지.
}
override func viewDidDisappear(_ animated: Bool) {
removeObservers()
mapController?.resetEngine() //엔진 정지. 추가되었던 ViewBase들이 삭제된다.
}
// 인증 실패시 호출.
func authenticationFailed(_ errorCode: Int, desc: String) {
print("error code: \(errorCode)")
print("desc: \(desc)")
_auth = false
switch errorCode {
case 400:
showToast(self.view, message: "지도 종료(API인증 파라미터 오류)")
break;
case 401:
showToast(self.view, message: "지도 종료(API인증 키 오류)")
break;
case 403:
showToast(self.view, message: "지도 종료(API인증 권한 오류)")
break;
case 429:
showToast(self.view, message: "지도 종료(API 사용쿼터 초과)")
break;
case 499:
showToast(self.view, message: "지도 종료(네트워크 오류) 5초 후 재시도..")
// 인증 실패 delegate 호출 이후 5초뒤에 재인증 시도..
DispatchQueue.main.asyncAfter(deadline: .now() + 5.0) {
print("retry auth...")
self.mapController?.prepareEngine()
}
break;
default:
break;
}
}
func addViews() {
//여기에서 그릴 View(KakaoMap, Roadview)들을 추가한다.
let defaultPosition: MapPoint = MapPoint(longitude: 127.108678, latitude: 37.402001)
//지도(KakaoMap)를 그리기 위한 viewInfo를 생성
let mapviewInfo: MapviewInfo = MapviewInfo(viewName: "mapview", viewInfoName: "map", defaultPosition: defaultPosition, defaultLevel: 7)
//KakaoMap 추가.
mapController?.addView(mapviewInfo)
}
//addView 성공 이벤트 delegate. 추가적으로 수행할 작업을 진행한다.
func addViewSucceeded(_ viewName: String, viewInfoName: String) {
print("OK") //추가 성공. 성공시 추가적으로 수행할 작업을 진행한다.
}
//addView 실패 이벤트 delegate. 실패에 대한 오류 처리를 진행한다.
func addViewFailed(_ viewName: String, viewInfoName: String) {
print("Failed")
}
//Container 뷰가 리사이즈 되었을때 호출된다. 변경된 크기에 맞게 ViewBase들의 크기를 조절할 필요가 있는 경우 여기에서 수행한다.
func containerDidResized(_ size: CGSize) {
let mapView: KakaoMap? = mapController?.getView("mapview") as? KakaoMap
mapView?.viewRect = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: size) //지도뷰의 크기를 리사이즈된 크기로 지정한다.
}
func viewWillDestroyed(_ view: ViewBase) {
}
func addObservers(){
NotificationCenter.default.addObserver(self, selector: #selector(willResignActive), name: UIApplication.willResignActiveNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(didBecomeActive), name: UIApplication.didBecomeActiveNotification, object: nil)
_observerAdded = true
}
func removeObservers(){
NotificationCenter.default.removeObserver(self, name: UIApplication.willResignActiveNotification, object: nil)
NotificationCenter.default.removeObserver(self, name: UIApplication.didBecomeActiveNotification, object: nil)
_observerAdded = false
}
@objc func willResignActive(){
mapController?.stopRendering() //뷰가 inactive 상태로 전환되는 경우 렌더링 중인 경우 렌더링을 중단.
}
@objc func didBecomeActive(){
mapController?.startRendering() //뷰가 active 상태가 되면 렌더링 시작. 엔진은 미리 시작된 상태여야 함.
}
func showToast(_ view: UIView, message: String, duration: TimeInterval = 2.0) {
let toastLabel = UILabel(frame: CGRect(x: view.frame.size.width/2 - 150, y: view.frame.size.height-100, width: 300, height: 35))
toastLabel.backgroundColor = UIColor.black
toastLabel.textColor = UIColor.white
toastLabel.textAlignment = NSTextAlignment.center;
view.addSubview(toastLabel)
toastLabel.text = message
toastLabel.alpha = 1.0
toastLabel.layer.cornerRadius = 10;
toastLabel.clipsToBounds = true
UIView.animate(withDuration: 0.4,
delay: duration - 0.4,
options: UIView.AnimationOptions.curveEaseOut,
animations: {
toastLabel.alpha = 0.0
},
completion: { (finished) in
toastLabel.removeFromSuperview()
})
}
var mapContainer: KMViewContainer?
var mapController: KMController?
var _observerAdded: Bool
var _auth: Bool
var _appear: Bool
}
Stroyboard를 통해 UIView를 생성하고, Custom class에 KMViewContainer를 입력하면 지도가 표시됩니다.
SwiftUI
KakaoMapsSDK에서 제공하는 지도를 표시하는 KMViewContainer는 UIKit의 UIView를 상속합니다. 따라서 SwiftUI에서 KakaoMapsSDK의 view를 표시하기 위해서는 이를 UIViewRepresentable로 Wrapping해야 사용할 수 있습니다. 또한 SwiftUI View의 라이프 사이클에 따라서 KMViewContainer를 컨트롤 하기 위해 KMControllerDelegate를 Coordinator로 구현할 수 있습니다.
아래 코드는 UIViewRepresentable을 이용하여 KMViewContainer를 Wrapping하고, Coordinator를 KMControllerDelegate로 구현한 예제입니다.
struct ContentView: View {
@State var draw: Bool = false //뷰의 appear 상태를 전달하기 위한 변수.
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)
context.coordinator.controller?.prepareEngine()
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()
}
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, KMControllerDelegate {
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(x: 14135167.020272, y: 4518393.389136)
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")
}
/// 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(x: 14135167.020272, y: 4518393.389136), zoomLevel: 10, mapView: mapView!)
mapView?.moveCamera(cameraUpdate)
first = false
}
}
var controller: KMController?
var first: Bool
}
}