@react-native-kakao/map 2.2.6 → 3.1.0-next.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (165) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +5 -38
  3. package/RNCKakaoMap.podspec +16 -52
  4. package/android/build.gradle +12 -107
  5. package/android/consumer-rules.pro +2 -0
  6. package/android/src/main/AndroidManifest.xml +3 -1
  7. package/android/src/main/java/net/mjstudio/rnkakao/map/CameraOptions.kt +61 -0
  8. package/android/src/main/java/net/mjstudio/rnkakao/map/KakaoMapModule.kt +131 -0
  9. package/android/src/main/java/net/mjstudio/rnkakao/map/KakaoMapView.kt +1257 -0
  10. package/android/src/main/java/net/mjstudio/rnkakao/map/MapCameraCommands.kt +306 -0
  11. package/android/src/main/java/net/mjstudio/rnkakao/map/MapDimScreen.kt +431 -0
  12. package/android/src/main/java/net/mjstudio/rnkakao/map/MapGui.kt +899 -0
  13. package/android/src/main/java/net/mjstudio/rnkakao/map/MapImageLoader.kt +232 -0
  14. package/android/src/main/java/net/mjstudio/rnkakao/map/MapLabels.kt +1572 -0
  15. package/android/src/main/java/net/mjstudio/rnkakao/map/MapOptions.kt +360 -0
  16. package/android/src/main/java/net/mjstudio/rnkakao/map/MapRoutes.kt +805 -0
  17. package/android/src/main/java/net/mjstudio/rnkakao/map/MapShapes.kt +1096 -0
  18. package/android/src/main/java/net/mjstudio/rnkakao/map/MapUtilities.kt +495 -0
  19. package/expo-module.config.json +5 -0
  20. package/ios/CameraOptions.swift +41 -0
  21. package/ios/KakaoMapModule.swift +204 -0
  22. package/ios/KakaoMapView.swift +1018 -0
  23. package/ios/MapCameraCommands.swift +451 -0
  24. package/ios/MapDimScreen.swift +463 -0
  25. package/ios/MapGui.swift +1099 -0
  26. package/ios/MapImageLoader.swift +177 -0
  27. package/ios/MapLabels.swift +1676 -0
  28. package/ios/MapOptions.swift +374 -0
  29. package/ios/MapRoutes.swift +903 -0
  30. package/ios/MapShapes.swift +1294 -0
  31. package/ios/MapUnits.swift +20 -0
  32. package/ios/MapUtilities.swift +365 -0
  33. package/lib/commonjs/index.js +180 -29
  34. package/lib/commonjs/index.js.map +1 -1
  35. package/lib/commonjs/index.web.js +18 -0
  36. package/lib/commonjs/index.web.js.map +1 -0
  37. package/lib/commonjs/package.json +1 -0
  38. package/lib/commonjs/types-assets.js +2 -0
  39. package/lib/commonjs/types-assets.js.map +1 -0
  40. package/lib/commonjs/types-camera.js +6 -0
  41. package/lib/commonjs/types-camera.js.map +1 -0
  42. package/lib/commonjs/types-dim.js +6 -0
  43. package/lib/commonjs/types-dim.js.map +1 -0
  44. package/lib/commonjs/types-gui.js +6 -0
  45. package/lib/commonjs/types-gui.js.map +1 -0
  46. package/lib/commonjs/types-labels.js +6 -0
  47. package/lib/commonjs/types-labels.js.map +1 -0
  48. package/lib/commonjs/types-routes.js +6 -0
  49. package/lib/commonjs/types-routes.js.map +1 -0
  50. package/lib/commonjs/types-shapes.js +6 -0
  51. package/lib/commonjs/types-shapes.js.map +1 -0
  52. package/lib/commonjs/types-utilities.js +6 -0
  53. package/lib/commonjs/types-utilities.js.map +1 -0
  54. package/lib/commonjs/types.js +6 -0
  55. package/lib/commonjs/types.js.map +1 -0
  56. package/lib/module/index.js +180 -22
  57. package/lib/module/index.js.map +1 -1
  58. package/lib/module/index.web.js +13 -0
  59. package/lib/module/index.web.js.map +1 -0
  60. package/lib/module/types-assets.js +2 -0
  61. package/lib/module/types-assets.js.map +1 -0
  62. package/lib/module/types-camera.js +4 -0
  63. package/lib/module/types-camera.js.map +1 -0
  64. package/lib/module/types-dim.js +4 -0
  65. package/lib/module/types-dim.js.map +1 -0
  66. package/lib/module/types-gui.js +4 -0
  67. package/lib/module/types-gui.js.map +1 -0
  68. package/lib/module/types-labels.js +4 -0
  69. package/lib/module/types-labels.js.map +1 -0
  70. package/lib/module/types-routes.js +4 -0
  71. package/lib/module/types-routes.js.map +1 -0
  72. package/lib/module/types-shapes.js +4 -0
  73. package/lib/module/types-shapes.js.map +1 -0
  74. package/lib/module/types-utilities.js +4 -0
  75. package/lib/module/types-utilities.js.map +1 -0
  76. package/lib/module/types.js +4 -0
  77. package/lib/module/types.js.map +1 -0
  78. package/lib/typescript/src/index.d.ts +59 -9
  79. package/lib/typescript/src/index.d.ts.map +1 -1
  80. package/lib/typescript/src/types-assets.d.ts +9 -0
  81. package/lib/typescript/src/types-assets.d.ts.map +1 -0
  82. package/lib/typescript/src/types-camera.d.ts +123 -0
  83. package/lib/typescript/src/types-camera.d.ts.map +1 -0
  84. package/lib/typescript/src/types-dim.d.ts +54 -0
  85. package/lib/typescript/src/types-dim.d.ts.map +1 -0
  86. package/lib/typescript/src/types-gui.d.ts +189 -0
  87. package/lib/typescript/src/types-gui.d.ts.map +1 -0
  88. package/lib/typescript/src/types-labels.d.ts +246 -0
  89. package/lib/typescript/src/types-labels.d.ts.map +1 -0
  90. package/lib/typescript/src/types-routes.d.ts +162 -0
  91. package/lib/typescript/src/types-routes.d.ts.map +1 -0
  92. package/lib/typescript/src/types-shapes.d.ts +253 -0
  93. package/lib/typescript/src/types-shapes.d.ts.map +1 -0
  94. package/lib/typescript/src/types-utilities.d.ts +130 -0
  95. package/lib/typescript/src/types-utilities.d.ts.map +1 -0
  96. package/lib/typescript/src/types.d.ts +222 -0
  97. package/lib/typescript/src/types.d.ts.map +1 -0
  98. package/package.json +24 -77
  99. package/src/index.tsx +322 -0
  100. package/src/index.web.tsx +46 -0
  101. package/src/types-assets.ts +8 -0
  102. package/src/types-camera.ts +97 -0
  103. package/src/types-dim.ts +54 -0
  104. package/src/types-gui.ts +171 -0
  105. package/src/types-labels.ts +207 -0
  106. package/src/types-routes.ts +118 -0
  107. package/src/types-shapes.ts +244 -0
  108. package/src/types-utilities.ts +115 -0
  109. package/src/types.ts +223 -0
  110. package/android/gradle.properties +0 -1
  111. package/android/src/main/AndroidManifestNew.xml +0 -2
  112. package/android/src/main/java/net/mjstudio/rnkakao/map/RNCKakaoMapModule.kt +0 -25
  113. package/android/src/main/java/net/mjstudio/rnkakao/map/RNCKakaoMapPackage.kt +0 -35
  114. package/android/src/newarch/KakaoMapSpec.kt +0 -7
  115. package/android/src/oldarch/KakaoMapSpec.kt +0 -14
  116. package/ios/Component/Map/RNCKakaoMapView.h +0 -23
  117. package/ios/Component/Map/RNCKakaoMapView.mm +0 -209
  118. package/ios/Component/Map/RNCKakaoMapViewManager.mm +0 -19
  119. package/ios/NativeModule/RNCKakaoMap.h +0 -7
  120. package/ios/NativeModule/RNCKakaoMap.mm +0 -19
  121. package/ios/NativeModule/RNCKakaoMapManager.swift +0 -7
  122. package/ios/Util/Util.h +0 -97
  123. package/lib/commonjs/component/KakaoMapView.js +0 -54
  124. package/lib/commonjs/component/KakaoMapView.js.map +0 -1
  125. package/lib/commonjs/spec/NativeKakaoMap.js +0 -9
  126. package/lib/commonjs/spec/NativeKakaoMap.js.map +0 -1
  127. package/lib/commonjs/spec/RNCKakaoMapViewNativeComponent.js +0 -15
  128. package/lib/commonjs/spec/RNCKakaoMapViewNativeComponent.js.map +0 -1
  129. package/lib/commonjs/type/type.js +0 -2
  130. package/lib/commonjs/type/type.js.map +0 -1
  131. package/lib/commonjs/util/Const.js +0 -14
  132. package/lib/commonjs/util/Const.js.map +0 -1
  133. package/lib/commonjs/util/Util.js +0 -15
  134. package/lib/commonjs/util/Util.js.map +0 -1
  135. package/lib/module/component/KakaoMapView.js +0 -48
  136. package/lib/module/component/KakaoMapView.js.map +0 -1
  137. package/lib/module/spec/NativeKakaoMap.js +0 -3
  138. package/lib/module/spec/NativeKakaoMap.js.map +0 -1
  139. package/lib/module/spec/RNCKakaoMapViewNativeComponent.js +0 -10
  140. package/lib/module/spec/RNCKakaoMapViewNativeComponent.js.map +0 -1
  141. package/lib/module/type/type.js +0 -2
  142. package/lib/module/type/type.js.map +0 -1
  143. package/lib/module/util/Const.js +0 -8
  144. package/lib/module/util/Const.js.map +0 -1
  145. package/lib/module/util/Util.js +0 -9
  146. package/lib/module/util/Util.js.map +0 -1
  147. package/lib/typescript/src/component/KakaoMapView.d.ts +0 -25
  148. package/lib/typescript/src/component/KakaoMapView.d.ts.map +0 -1
  149. package/lib/typescript/src/spec/NativeKakaoMap.d.ts +0 -7
  150. package/lib/typescript/src/spec/NativeKakaoMap.d.ts.map +0 -1
  151. package/lib/typescript/src/spec/RNCKakaoMapViewNativeComponent.d.ts +0 -32
  152. package/lib/typescript/src/spec/RNCKakaoMapViewNativeComponent.d.ts.map +0 -1
  153. package/lib/typescript/src/type/type.d.ts +0 -8
  154. package/lib/typescript/src/type/type.d.ts.map +0 -1
  155. package/lib/typescript/src/util/Const.d.ts +0 -8
  156. package/lib/typescript/src/util/Const.d.ts.map +0 -1
  157. package/lib/typescript/src/util/Util.d.ts +0 -3
  158. package/lib/typescript/src/util/Util.d.ts.map +0 -1
  159. package/src/component/KakaoMapView.tsx +0 -82
  160. package/src/index.ts +0 -43
  161. package/src/spec/NativeKakaoMap.ts +0 -8
  162. package/src/spec/RNCKakaoMapViewNativeComponent.ts +0 -38
  163. package/src/type/type.ts +0 -7
  164. package/src/util/Const.ts +0 -7
  165. package/src/util/Util.ts +0 -11
@@ -0,0 +1,1018 @@
1
+ import ExpoModulesCore
2
+ import KakaoMapsSDK
3
+
4
+ final class KakaoMapView: ExpoView, MapControllerDelegate, KakaoMapEventDelegate {
5
+ let onMapReady = EventDispatcher()
6
+ let onError = EventDispatcher()
7
+ let onCameraMoveStart = EventDispatcher()
8
+ let onCameraIdle = EventDispatcher()
9
+ let onMapClick = EventDispatcher()
10
+ let onMapLongClick = EventDispatcher()
11
+ let onTerrainClick = EventDispatcher()
12
+ let onViewportClick = EventDispatcher()
13
+ let onVisibilityChanged = EventDispatcher()
14
+ let onFocusChanged = EventDispatcher()
15
+ let onPoiClick = EventDispatcher()
16
+ let onCompassClick = EventDispatcher()
17
+ let onMapTypeChanged = EventDispatcher()
18
+ let onPaddingChanged = EventDispatcher()
19
+ let onViewportChanged = EventDispatcher()
20
+ let onLabelClick = EventDispatcher()
21
+ let onLabelAnimationStopped = EventDispatcher()
22
+ let onLabelsChanged = EventDispatcher()
23
+ let onShapeAnimationStop = EventDispatcher()
24
+ let onShapesChanged = EventDispatcher()
25
+ let onRoutesChanged = EventDispatcher()
26
+ let onRouteProgress = EventDispatcher()
27
+ let onRouteAnimationStop = EventDispatcher()
28
+ let onGuiEvent = EventDispatcher()
29
+ let onDimScreenEvent = EventDispatcher()
30
+ let onCameraOperation = EventDispatcher()
31
+ private var labels: MapLabels?
32
+ private var shapes: MapShapes?
33
+ private var routes: MapRoutes?
34
+ private var gui: MapGui?
35
+ private var dimScreen: MapDimScreen?
36
+ private var utilities: MapUtilities?
37
+ private var cameraCommands: MapCameraCommands?
38
+ private var imageLoader: MapImageLoader?
39
+ private var pendingLabels: MapLabelSceneRecord?
40
+ private var pendingShapes: [String: Any]?
41
+ private var hasUsedShapes = false
42
+ private var pendingRoutes: [String: Any]?
43
+ private var hasUsedRoutes = false
44
+ private var routeGeneration = 0
45
+ private var labelGeneration = 0
46
+ var pendingInitialCamera: CameraOptions?
47
+ var camera: CameraOptions?
48
+ private var initialCamera: CameraOptions?
49
+ private var lastCameraValues: [Double]?
50
+ private var pendingOptions: KakaoMapViewOptions?
51
+ private var appliedOptions = KakaoMapViewOptions()
52
+ private var semanticOverlays: Set<String> = []
53
+ private var customOverlays: Set<String> = []
54
+ private var currentViewInfoName = "map"
55
+ private var currentLanguage = "ko"
56
+ private var hasInitialProps = false
57
+ private var container: KMViewContainer?
58
+ private var controller: KMController?
59
+ private var map: KakaoMap?
60
+ private var lastEmittedViewportSize: CGSize?
61
+ private var isStarted = false
62
+ private var isDestroyed = false
63
+ private var isAuthenticated = false
64
+ private var isReady = false
65
+ private let viewCreationError = "ERR_KAKAO_MAP_VIEW_CREATION"
66
+
67
+ required init(appContext: AppContext? = nil) {
68
+ super.init(appContext: appContext)
69
+ NotificationCenter.default.addObserver(
70
+ self,
71
+ selector: #selector(pause),
72
+ name: UIApplication.willResignActiveNotification,
73
+ object: nil
74
+ )
75
+ NotificationCenter.default.addObserver(
76
+ self,
77
+ selector: #selector(resume),
78
+ name: UIApplication.didBecomeActiveNotification,
79
+ object: nil
80
+ )
81
+ }
82
+
83
+ func updateProps() {
84
+ if !hasInitialProps {
85
+ hasInitialProps = true
86
+ initialCamera = pendingInitialCamera
87
+ }
88
+ if isReady, camera?.values != lastCameraValues {
89
+ if let camera {
90
+ applyCameraProp(camera)
91
+ }
92
+ lastCameraValues = camera?.values
93
+ }
94
+ applyPendingOptionsIfReady()
95
+ applyPendingShapesIfReady()
96
+ applyPendingRoutesIfReady()
97
+ applyPendingLabelsIfReady()
98
+ }
99
+
100
+ func queueShapes(_ scene: [String: Any]?) {
101
+ if scene == nil, !hasUsedShapes {
102
+ return
103
+ }
104
+ hasUsedShapes = true
105
+ pendingShapes = scene ?? [:]
106
+ }
107
+
108
+ func queueLabels(_ scene: MapLabelSceneRecord?) {
109
+ if scene == nil, labels == nil {
110
+ pendingLabels = nil
111
+ return
112
+ }
113
+ pendingLabels = scene ?? MapLabelSceneRecord()
114
+ }
115
+
116
+ func queueRoutes(_ scene: [String: Any]?) {
117
+ if scene == nil, !hasUsedRoutes {
118
+ return
119
+ }
120
+ hasUsedRoutes = true
121
+ pendingRoutes = scene ?? [:]
122
+ }
123
+
124
+ func queueOptions(_ options: KakaoMapViewOptions?) {
125
+ guard let options else { return }
126
+ guard options.mapType == nil || options.viewInfo == nil else {
127
+ onError([
128
+ "code": invalidCameraArgument,
129
+ "message": "mapType and viewInfo are mutually exclusive."
130
+ ])
131
+ return
132
+ }
133
+ let value = pendingOptions ?? KakaoMapViewOptions()
134
+ value.merge(options)
135
+ pendingOptions = value
136
+ }
137
+
138
+ override func layoutSubviews() {
139
+ super.layoutSubviews()
140
+ if container?.frame != bounds {
141
+ container?.frame = bounds
142
+ }
143
+ startIfNeeded()
144
+ }
145
+
146
+ override func didMoveToWindow() {
147
+ super.didMoveToWindow()
148
+ guard window != nil else {
149
+ // The SDK retains its event delegate; detach it so an unmounted view can deinitialize.
150
+ map?.eventDelegate = nil
151
+ pause()
152
+ return
153
+ }
154
+ map?.eventDelegate = self
155
+ startIfNeeded()
156
+ }
157
+
158
+ private func startIfNeeded() {
159
+ guard !isDestroyed, window != nil, bounds.width > 0, bounds.height > 0 else { return }
160
+ if !isStarted {
161
+ isStarted = true
162
+ guard KakaoMapModule.appKey != nil else {
163
+ onError([
164
+ "code": mapNotInitialized,
165
+ "message": "Call initializeKakaoMapSDK before mounting the map."
166
+ ])
167
+ return
168
+ }
169
+ // Creating a zero-sized container prevents the SDK engine from preparing.
170
+ let container = KMViewContainer(frame: bounds)
171
+ self.container = container
172
+ addSubview(container)
173
+ let controller = KMController(viewContainer: container)
174
+ self.controller = controller
175
+ controller.delegate = self
176
+ guard controller.prepareEngine() else {
177
+ onError([
178
+ "code": viewCreationError,
179
+ "message": "Could not prepare the map engine."
180
+ ])
181
+ return
182
+ }
183
+ }
184
+ resume()
185
+ }
186
+
187
+ @objc private func pause() {
188
+ controller?.pauseEngine()
189
+ }
190
+
191
+ @objc private func resume() {
192
+ if !isDestroyed, window != nil, UIApplication.shared.applicationState == .active,
193
+ controller?.isEnginePrepared == true, controller?.isEngineActive == false
194
+ {
195
+ controller?.activateEngine()
196
+ }
197
+ }
198
+
199
+ func addViews() {
200
+ guard !isDestroyed else { return }
201
+ controller?.addView(MapviewInfo(
202
+ viewName: "kakao-map",
203
+ viewInfoName: "map",
204
+ defaultPosition: MapPoint(longitude: 127.108678, latitude: 37.402001),
205
+ defaultLevel: 15
206
+ ))
207
+ }
208
+
209
+ func addViewSucceeded(_ viewName: String, viewInfoName: String) {
210
+ guard !isDestroyed, let map = controller?.getView(viewName) as? KakaoMap else { return }
211
+ self.map = map
212
+ currentViewInfoName = viewInfoName
213
+ let supportedLanguages = map.getSupportedLanguages()
214
+ if !supportedLanguages.contains(currentLanguage) {
215
+ currentLanguage = supportedLanguages.contains("ko") ? "ko" : supportedLanguages.first ?? ""
216
+ }
217
+ if !currentLanguage.isEmpty {
218
+ map.setLanguage(currentLanguage)
219
+ }
220
+ map.eventDelegate = window != nil ? self : nil
221
+ containerDidResized(bounds.size)
222
+ notifyReady()
223
+ }
224
+
225
+ private func notifyReady() {
226
+ guard !isDestroyed, !isReady, isAuthenticated, map != nil else { return }
227
+ isReady = true
228
+ if let camera = camera ?? initialCamera {
229
+ applyCameraProp(camera)
230
+ }
231
+ lastCameraValues = camera?.values
232
+ applyPendingOptionsIfReady()
233
+ applyPendingShapesIfReady()
234
+ applyPendingRoutesIfReady()
235
+ applyPendingLabelsIfReady()
236
+ onMapReady([:])
237
+ }
238
+
239
+ func addViewFailed(_ viewName: String, viewInfoName: String) {
240
+ guard !isDestroyed else { return }
241
+ onError(["code": viewCreationError, "message": "Could not create the map view."])
242
+ }
243
+
244
+ func authenticationFailed(_ errorCode: Int, desc: String) {
245
+ guard !isDestroyed else { return }
246
+ releaseMapObjects()
247
+ map?.eventDelegate = nil
248
+ map = nil
249
+ isReady = false
250
+ isAuthenticated = false
251
+ onError(["code": String(errorCode), "message": desc])
252
+ }
253
+
254
+ func authenticationSucceeded() {
255
+ isAuthenticated = true
256
+ resume()
257
+ notifyReady()
258
+ }
259
+
260
+ func containerDidResized(_ size: CGSize) {
261
+ guard let map, map.viewRect.size != size else { return }
262
+ map.viewRect = CGRect(origin: .zero, size: size)
263
+ }
264
+
265
+ func moveCamera(_ camera: CameraOptions, duration: Double) throws {
266
+ let map = try requireMap()
267
+ try dispatchCameraUpdate(camera.update(for: map), on: map, duration: duration)
268
+ }
269
+
270
+ private func dispatchCameraUpdate(
271
+ _ update: CameraUpdate,
272
+ on map: KakaoMap,
273
+ duration: Double
274
+ ) throws {
275
+ guard duration.isFinite, duration >= 0, duration <= Double(Int32.max),
276
+ duration.rounded() == duration
277
+ else {
278
+ throw KakaoMapException(
279
+ invalidCameraArgument,
280
+ "Duration must be an integer between 0 and 2147483647 milliseconds."
281
+ )
282
+ }
283
+ cameraCommands?.supersedePendingOperations()
284
+ if duration == 0 {
285
+ map.moveCamera(update)
286
+ } else {
287
+ map.animateCamera(
288
+ cameraUpdate: update,
289
+ options: CameraAnimationOptions(
290
+ autoElevation: false,
291
+ consecutive: false,
292
+ durationInMillis: UInt(duration)
293
+ )
294
+ )
295
+ }
296
+ }
297
+
298
+ func getCamera() throws -> [String: Any] {
299
+ try cameraPayload(requireMap())
300
+ }
301
+
302
+ func updateCamera(_ options: CameraUpdateOptions, duration: Double) throws {
303
+ let map = try requireMap()
304
+ try dispatchCameraUpdate(options.update(for: map), on: map, duration: duration)
305
+ }
306
+
307
+ func getMapInfo() throws -> [String: Any] {
308
+ let map = try requireMap()
309
+ return [
310
+ "mapType": currentViewInfoName == "skyview" ? "skyview" : "normal",
311
+ "viewInfoName": currentViewInfoName,
312
+ "zoomRange": ["min": map.cameraMinLevel, "max": map.cameraMaxLevel],
313
+ "padding": [
314
+ "top": map.margins.top,
315
+ "right": map.margins.right,
316
+ "bottom": map.margins.bottom,
317
+ "left": map.margins.left
318
+ ],
319
+ "size": ["width": map.viewRect.width, "height": map.viewRect.height],
320
+ "viewport": [
321
+ "x": map.viewRect.origin.x,
322
+ "y": map.viewRect.origin.y,
323
+ "width": map.viewRect.width,
324
+ "height": map.viewRect.height
325
+ ],
326
+ "language": currentLanguage,
327
+ "supportedLanguages": map.getSupportedLanguages(),
328
+ "capabilities": ["coordinateToScreen": false]
329
+ ]
330
+ }
331
+
332
+ func screenToCoordinate(_ point: MapScreenPoint) throws -> [String: Double] {
333
+ let map = try requireMap()
334
+ guard point.x.isFinite, point.y.isFinite,
335
+ point.x >= 0, point.y >= 0,
336
+ point.x <= map.viewRect.width, point.y <= map.viewRect.height
337
+ else {
338
+ throw KakaoMapException(
339
+ invalidCameraArgument,
340
+ "Screen point must be finite and inside the map view."
341
+ )
342
+ }
343
+ let coordinate = map.getPosition(CGPoint(x: point.x, y: point.y)).wgsCoord
344
+ return ["latitude": coordinate.latitude, "longitude": coordinate.longitude]
345
+ }
346
+
347
+ func clearCache() throws {
348
+ _ = try requireUtilities().command(["type": "clearCache", "kind": "all"])
349
+ }
350
+
351
+ func requireUtilities() throws -> MapUtilities {
352
+ let map = try requireMap()
353
+ if let utilities {
354
+ return utilities
355
+ }
356
+ guard let controller else {
357
+ throw KakaoMapException("ERR_KAKAO_MAP_NOT_READY", "The map controller is unavailable.")
358
+ }
359
+ let manager = MapUtilities(map: map, controller: controller)
360
+ utilities = manager
361
+ return manager
362
+ }
363
+
364
+ func executeCameraCommand(_ command: [String: Any]) throws -> [String: Any] {
365
+ let map = try requireMap()
366
+ if let cameraCommands {
367
+ return try cameraCommands.command(command)
368
+ }
369
+ let manager = MapCameraCommands(map: map) { [weak self] payload in
370
+ guard let self, !isDestroyed else { return }
371
+ onCameraOperation(payload)
372
+ }
373
+ cameraCommands = manager
374
+ return try manager.command(command)
375
+ }
376
+
377
+ func runLabelCommand(_ command: MapLabelCommandRecord) throws {
378
+ try requireLabels().command(command)
379
+ }
380
+
381
+ func getLabelCapabilities() throws -> [String: Any] {
382
+ try requireLabels().capabilities()
383
+ }
384
+
385
+ func getLabelState() throws -> [String: Any] {
386
+ try requireLabels().state()
387
+ }
388
+
389
+ func applyShapes(_ scene: [String: Any]) throws -> [String: Any] {
390
+ pendingShapes = nil
391
+ let counts = try requireShapes().apply(scene)
392
+ hasUsedShapes = true
393
+ if !isDestroyed {
394
+ onShapesChanged(counts)
395
+ }
396
+ return counts
397
+ }
398
+
399
+ func executeShapeCommand(_ command: [String: Any]) throws -> [String: Any] {
400
+ try requireShapes().command(command)
401
+ }
402
+
403
+ private func requireShapes() throws -> MapShapes {
404
+ let map = try requireMap()
405
+ if let shapes {
406
+ return shapes
407
+ }
408
+ let shapes = MapShapes(map: map) { [weak self] event, payload in
409
+ guard let self, !isDestroyed else { return }
410
+ if event == "onShapeAnimationStop" {
411
+ onShapeAnimationStop(payload)
412
+ }
413
+ }
414
+ self.shapes = shapes
415
+ return shapes
416
+ }
417
+
418
+ private func requireLabels() throws -> MapLabels {
419
+ let map = try requireMap()
420
+ if let labels {
421
+ return labels
422
+ }
423
+ let loader = requireImageLoader()
424
+ let labels = MapLabels(
425
+ map: map,
426
+ imageLoader: loader,
427
+ emitClick: { [weak self] event in
428
+ guard let self, !isDestroyed else { return }
429
+ onLabelClick(event)
430
+ },
431
+ emitAnimationStopped: { [weak self] event in
432
+ guard let self, !isDestroyed else { return }
433
+ onLabelAnimationStopped(event)
434
+ },
435
+ resolveShape: { [weak self] id in
436
+ self?.shapes?.resolveShape(id)
437
+ }
438
+ )
439
+ self.labels = labels
440
+ return labels
441
+ }
442
+
443
+ func applyRoutes(_ scene: [String: Any], promise: Promise) throws {
444
+ pendingRoutes = nil
445
+ hasUsedRoutes = true
446
+ routeGeneration += 1
447
+ let generation = routeGeneration
448
+ try requireRoutes().apply(scene) { [weak self] result in
449
+ switch result {
450
+ case let .success(counts):
451
+ if let self, !isDestroyed, generation == routeGeneration {
452
+ onRoutesChanged(counts)
453
+ }
454
+ promise.resolve(counts)
455
+ case let .failure(error):
456
+ promise.reject(error)
457
+ }
458
+ }
459
+ }
460
+
461
+ func executeRouteCommand(_ command: [String: Any]) throws -> [String: Any] {
462
+ try requireRoutes().command(command)
463
+ }
464
+
465
+ private func requireRoutes() throws -> MapRoutes {
466
+ let map = try requireMap()
467
+ if let routes {
468
+ return routes
469
+ }
470
+ let manager = MapRoutes(
471
+ map: map,
472
+ imageLoader: requireImageLoader()
473
+ ) { [weak self] event, payload in
474
+ guard let self, !isDestroyed else { return }
475
+ switch event {
476
+ case "onRouteProgress": onRouteProgress(payload)
477
+ case "onRouteAnimationStop": onRouteAnimationStop(payload)
478
+ default: break
479
+ }
480
+ }
481
+ routes = manager
482
+ return manager
483
+ }
484
+
485
+ func requireGui() throws -> MapGui {
486
+ let map = try requireMap()
487
+ if let gui {
488
+ return gui
489
+ }
490
+ let manager = MapGui(map: map, imageLoader: requireImageLoader()) { [weak self] payload in
491
+ guard let self, !isDestroyed else { return }
492
+ onGuiEvent(payload)
493
+ }
494
+ gui = manager
495
+ return manager
496
+ }
497
+
498
+ func requireDimScreen() throws -> MapDimScreen {
499
+ let map = try requireMap()
500
+ if let dimScreen {
501
+ return dimScreen
502
+ }
503
+ let manager = MapDimScreen(map: map) { [weak self] payload in
504
+ guard let self, !isDestroyed else { return }
505
+ onDimScreenEvent(payload)
506
+ }
507
+ dimScreen = manager
508
+ return manager
509
+ }
510
+
511
+ private func requireImageLoader() -> MapImageLoader {
512
+ if let imageLoader {
513
+ return imageLoader
514
+ }
515
+ let loader = MapImageLoader(appContext: appContext)
516
+ imageLoader = loader
517
+ return loader
518
+ }
519
+
520
+ private func applyPendingRoutesIfReady() {
521
+ guard isReady, let scene = pendingRoutes else { return }
522
+ pendingRoutes = nil
523
+ routeGeneration += 1
524
+ let generation = routeGeneration
525
+ do {
526
+ try requireRoutes().apply(scene) { [weak self] result in
527
+ guard let self, !isDestroyed, generation == routeGeneration else { return }
528
+ switch result {
529
+ case let .success(counts): onRoutesChanged(counts)
530
+ case let .failure(error): reportObjectError(error, fallbackCode: "ERR_KAKAO_MAP_ROUTE")
531
+ }
532
+ }
533
+ } catch {
534
+ reportObjectError(error, fallbackCode: "ERR_KAKAO_MAP_ROUTE")
535
+ }
536
+ }
537
+
538
+ private func reportObjectError(_ error: Error, fallbackCode: String) {
539
+ if let error = error as? Exception {
540
+ onError(["code": error.code, "message": error.reason])
541
+ } else {
542
+ onError(["code": fallbackCode, "message": error.localizedDescription])
543
+ }
544
+ }
545
+
546
+ private func applyPendingLabelsIfReady() {
547
+ guard isReady, let scene = pendingLabels else { return }
548
+ pendingLabels = nil
549
+ let previousGeneration = labelGeneration
550
+ labelGeneration += 1
551
+ let generation = labelGeneration
552
+ do {
553
+ try requireLabels().apply(scene) { [weak self] result in
554
+ guard let self, !isDestroyed, generation == labelGeneration else { return }
555
+ switch result {
556
+ case .success:
557
+ if let labels {
558
+ onLabelsChanged(labels.state())
559
+ }
560
+ case let .failure(error):
561
+ reportLabelError(error)
562
+ }
563
+ }
564
+ } catch {
565
+ if labelGeneration == generation {
566
+ labelGeneration = previousGeneration
567
+ }
568
+ reportLabelError(error)
569
+ }
570
+ }
571
+
572
+ private func reportLabelError(_ error: Error) {
573
+ if let error = error as? Exception {
574
+ onError(["code": error.code, "message": error.reason])
575
+ } else {
576
+ onError(["code": "ERR_KAKAO_MAP_LABEL", "message": error.localizedDescription])
577
+ }
578
+ }
579
+
580
+ private func applyPendingShapesIfReady() {
581
+ guard isReady, let scene = pendingShapes else { return }
582
+ pendingShapes = nil
583
+ do {
584
+ _ = try applyShapes(scene)
585
+ } catch {
586
+ reportShapeError(error)
587
+ }
588
+ }
589
+
590
+ private func reportShapeError(_ error: Error) {
591
+ if let error = error as? Exception {
592
+ onError(["code": error.code, "message": error.reason])
593
+ } else {
594
+ onError(["code": "ERR_KAKAO_MAP_SHAPE", "message": error.localizedDescription])
595
+ }
596
+ }
597
+
598
+ private func releaseMapObjects() {
599
+ labelGeneration += 1
600
+ routeGeneration += 1
601
+ utilities?.dispose()
602
+ utilities = nil
603
+ cameraCommands?.dispose()
604
+ cameraCommands = nil
605
+ labels?.dispose()
606
+ labels = nil
607
+ gui?.dispose()
608
+ gui = nil
609
+ dimScreen?.dispose()
610
+ dimScreen = nil
611
+ routes?.dispose()
612
+ routes = nil
613
+ shapes?.dispose()
614
+ shapes = nil
615
+ imageLoader?.dispose()
616
+ imageLoader = nil
617
+ }
618
+
619
+ private func applyPendingOptionsIfReady() {
620
+ guard isReady, let map, let pendingOptions else { return }
621
+ self.pendingOptions = nil
622
+ let target = appliedOptions.copy()
623
+ target.merge(pendingOptions)
624
+ do {
625
+ try validate(target, for: map)
626
+ apply(target.difference(from: appliedOptions), to: map)
627
+ appliedOptions = target
628
+ } catch let error as Exception {
629
+ onError(["code": error.code, "message": error.reason])
630
+ } catch {
631
+ onError(["code": invalidCameraArgument, "message": error.localizedDescription])
632
+ }
633
+ }
634
+
635
+ private func validate(_ options: KakaoMapViewOptions, for map: KakaoMap) throws {
636
+ if options.mapType != nil, options.viewInfo != nil {
637
+ throw KakaoMapException(
638
+ invalidCameraArgument,
639
+ "mapType and viewInfo are mutually exclusive."
640
+ )
641
+ }
642
+ if let mapType = options.mapType, mapType != "normal", mapType != "skyview" {
643
+ throw KakaoMapException(invalidCameraArgument, "Invalid map type.")
644
+ }
645
+ if let viewInfo = options.viewInfo {
646
+ guard !viewInfo.name.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty,
647
+ viewInfo.appName?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty != true
648
+ else {
649
+ throw KakaoMapException(invalidCameraArgument, "View info names must not be empty.")
650
+ }
651
+ }
652
+ if let overlays = options.overlays {
653
+ for overlay in overlays {
654
+ _ = try overlayID(overlay)
655
+ }
656
+ }
657
+ if let overlays = options.customOverlays,
658
+ overlays.contains(where: { $0.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty })
659
+ {
660
+ throw KakaoMapException(invalidCameraArgument, "Custom overlay IDs must not be empty.")
661
+ }
662
+ if let language = options.language, !map.getSupportedLanguages().contains(language) {
663
+ throw KakaoMapException(
664
+ invalidCameraArgument,
665
+ "The map does not support language '\(language)'."
666
+ )
667
+ }
668
+ if let scale = options.poi?.scale {
669
+ _ = try poiScale(scale)
670
+ }
671
+ if let scale = options.buildingScale,
672
+ !scale.isFinite || !(0 ... 1).contains(scale)
673
+ {
674
+ throw KakaoMapException(invalidCameraArgument, "Building scale must be between 0 and 1.")
675
+ }
676
+ if let padding = options.padding {
677
+ let values = [padding.top, padding.right, padding.bottom, padding.left]
678
+ guard values.allSatisfy({ $0.isFinite && $0 >= 0 }) else {
679
+ throw KakaoMapException(
680
+ invalidCameraArgument,
681
+ "Map padding must contain non-negative values."
682
+ )
683
+ }
684
+ }
685
+ if let range = options.zoomRange {
686
+ guard range.min.isFinite, range.max.isFinite,
687
+ range.min.rounded() == range.min, range.max.rounded() == range.max,
688
+ range.min >= Double(map.minLevel), range.max <= Double(map.maxLevel),
689
+ range.min <= range.max
690
+ else {
691
+ throw KakaoMapException(invalidCameraArgument, "Invalid map zoom range.")
692
+ }
693
+ }
694
+ if let gestures = options.gestures {
695
+ for gesture in gestures.keys {
696
+ _ = try gestureType(gesture)
697
+ }
698
+ }
699
+ _ = try options.compass?.position?.values()
700
+ _ = try options.scaleBar?.position?.values()
701
+ _ = try options.logo?.values()
702
+ if let fade = options.scaleBar?.fade {
703
+ _ = try duration(fade.inDuration)
704
+ _ = try duration(fade.outDuration)
705
+ _ = try duration(fade.retentionDuration)
706
+ }
707
+ }
708
+
709
+ private func apply(_ options: KakaoMapViewOptions, to map: KakaoMap) {
710
+ if let viewInfo = options.viewInfo {
711
+ map.changeViewInfo(appName: viewInfo.appName ?? "openmap", viewInfoName: viewInfo.name)
712
+ } else if let mapType = options.mapType {
713
+ map.changeViewInfo(
714
+ appName: "openmap",
715
+ viewInfoName: mapType == "skyview" ? "skyview" : "map"
716
+ )
717
+ }
718
+
719
+ let previousOverlays = semanticOverlays.union(customOverlays)
720
+ if let overlays = options.overlays {
721
+ semanticOverlays = Set(overlays.compactMap { try? overlayID($0) })
722
+ }
723
+ if let overlays = options.customOverlays {
724
+ customOverlays = Set(overlays)
725
+ }
726
+ let nextOverlays = semanticOverlays.union(customOverlays)
727
+ for overlay in previousOverlays.subtracting(nextOverlays) {
728
+ map.hideOverlay(overlay)
729
+ }
730
+ for overlay in nextOverlays.subtracting(previousOverlays) {
731
+ map.showOverlay(overlay)
732
+ }
733
+
734
+ if let language = options.language {
735
+ map.setLanguage(language)
736
+ currentLanguage = language
737
+ }
738
+ if let visible = options.poi?.visible {
739
+ map.setPoiEnabled(visible)
740
+ }
741
+ if let clickable = options.poi?.clickable {
742
+ map.poiClickable = clickable
743
+ }
744
+ if let scale = options.poi?.scale, let value = try? poiScale(scale) {
745
+ map.poiScale = value
746
+ }
747
+ if let scale = options.buildingScale {
748
+ map.buildingScale = Float(scale)
749
+ }
750
+ if let padding = options.padding {
751
+ map.setMargins(UIEdgeInsets(
752
+ top: padding.top,
753
+ left: padding.left,
754
+ bottom: padding.bottom,
755
+ right: padding.right
756
+ ))
757
+ }
758
+ if let range = options.zoomRange {
759
+ let minLevel = Int(range.min)
760
+ let maxLevel = Int(range.max)
761
+ if minLevel > map.cameraMaxLevel {
762
+ map.cameraMaxLevel = maxLevel
763
+ } else if maxLevel < map.cameraMinLevel {
764
+ map.cameraMinLevel = minLevel
765
+ }
766
+ map.cameraMinLevel = minLevel
767
+ map.cameraMaxLevel = maxLevel
768
+ }
769
+ if let gestures = options.gestures {
770
+ for (gesture, isEnabled) in gestures {
771
+ if let value = try? gestureType(gesture) {
772
+ map.setGestureEnable(type: value, enable: isEnabled)
773
+ }
774
+ }
775
+ }
776
+ if let compass = options.compass {
777
+ if let visible = compass.visible {
778
+ visible ? map.showCompass() : map.hideCompass()
779
+ }
780
+ if let position = compass.position, let (origin, offset) = try? position.values() {
781
+ map.setCompassPosition(origin: origin, position: offset)
782
+ }
783
+ }
784
+ if let scaleBar = options.scaleBar {
785
+ if let visible = scaleBar.visible {
786
+ visible ? map.showScaleBar() : map.hideScaleBar()
787
+ }
788
+ if let position = scaleBar.position, let (origin, offset) = try? position.values() {
789
+ map.setScaleBarPosition(origin: origin, position: offset)
790
+ }
791
+ if let autoHide = scaleBar.autoHide {
792
+ map.setScaleBarAutoDisappear(autoHide)
793
+ }
794
+ if let fade = scaleBar.fade,
795
+ let fadeIn = try? duration(fade.inDuration),
796
+ let fadeOut = try? duration(fade.outDuration),
797
+ let retention = try? duration(fade.retentionDuration)
798
+ {
799
+ map.setScaleBarFadeInOutOption(FadeInOutOptions(
800
+ fadeInTime: fadeIn,
801
+ fadeOutTime: fadeOut,
802
+ retentionTime: retention
803
+ ))
804
+ }
805
+ }
806
+ if let logo = options.logo, let (origin, offset) = try? logo.values() {
807
+ map.setLogoPosition(origin: origin, position: offset)
808
+ }
809
+ }
810
+
811
+ private func overlayID(_ overlay: String) throws -> String {
812
+ switch overlay {
813
+ case "roadviewLine": "roadview_line"
814
+ case "hillShading": "hill_shading"
815
+ case "bicycleRoad": "bicycle_road"
816
+ case "hybrid": "hybrid"
817
+ default: throw KakaoMapException(invalidCameraArgument, "Invalid map overlay.")
818
+ }
819
+ }
820
+
821
+ private func poiScale(_ scale: String) throws -> PoiScaleType {
822
+ switch scale {
823
+ case "small": .small
824
+ case "regular": .regular
825
+ case "large": .large
826
+ case "xLarge": .xLarge
827
+ default: throw KakaoMapException(invalidCameraArgument, "Invalid map POI scale.")
828
+ }
829
+ }
830
+
831
+ private func gestureType(_ gesture: String) throws -> GestureType {
832
+ switch gesture {
833
+ case "doubleTapZoomIn": .doubleTapZoomIn
834
+ case "twoFingerTapZoomOut": .twoFingerTapZoomOut
835
+ case "pan": .pan
836
+ case "rotate": .rotate
837
+ case "zoom": .zoom
838
+ case "tilt": .tilt
839
+ case "longTapAndDrag": .longTapAndDrag
840
+ case "rotateZoom": .rotateZoom
841
+ case "oneFingerZoom": .oneFingerZoom
842
+ default: throw KakaoMapException(invalidCameraArgument, "Invalid map gesture.")
843
+ }
844
+ }
845
+
846
+ private func duration(_ value: Double) throws -> UInt32 {
847
+ guard value.isFinite, value >= 0, value <= Double(UInt32.max), value.rounded() == value else {
848
+ throw KakaoMapException(
849
+ invalidCameraArgument,
850
+ "Widget durations must be integer milliseconds between 0 and \(UInt32.max)."
851
+ )
852
+ }
853
+ return UInt32(value)
854
+ }
855
+
856
+ private func requireMap() throws -> KakaoMap {
857
+ if isDestroyed {
858
+ throw KakaoMapException(
859
+ "ERR_KAKAO_MAP_VIEW_DESTROYED",
860
+ "The map view was destroyed."
861
+ )
862
+ }
863
+ if KakaoMapModule.appKey == nil {
864
+ throw KakaoMapException(
865
+ mapNotInitialized,
866
+ "Initialize the map SDK first."
867
+ )
868
+ }
869
+ guard isReady, let map else { throw KakaoMapException(
870
+ "ERR_KAKAO_MAP_NOT_READY",
871
+ "Wait for onMapReady before calling map methods."
872
+ ) }
873
+ return map
874
+ }
875
+
876
+ private func applyCameraProp(_ camera: CameraOptions) {
877
+ do {
878
+ try moveCamera(camera, duration: 0)
879
+ } catch let error as Exception {
880
+ onError(["code": error.code, "message": error.reason])
881
+ } catch {
882
+ onError(["code": "ERR_KAKAO_MAP_CAMERA", "message": error.localizedDescription])
883
+ }
884
+ }
885
+
886
+ private func cameraPayload(_ map: KakaoMap) -> [String: Any] {
887
+ let margins = map.margins
888
+ let point = map.getPosition(CGPoint(
889
+ x: (margins.left + map.viewRect.width - margins.right) / 2,
890
+ y: (margins.top + map.viewRect.height - margins.bottom) / 2
891
+ ))
892
+ let rotation = map.rotationAngle * 180 / .pi
893
+ return [
894
+ "latitude": point.wgsCoord.latitude,
895
+ "longitude": point.wgsCoord.longitude,
896
+ "zoomLevel": map.zoomLevel,
897
+ "tilt": map.tiltAngle * 180 / .pi,
898
+ "rotation": (rotation.truncatingRemainder(dividingBy: 360) + 360)
899
+ .truncatingRemainder(dividingBy: 360)
900
+ ]
901
+ }
902
+
903
+ func cameraWillMove(kakaoMap: KakaoMap, by: MoveBy) {
904
+ if !isDestroyed {
905
+ if by != .notUserAction {
906
+ cameraCommands?.supersedePendingOperations()
907
+ }
908
+ onCameraMoveStart(["reason": reason(by)])
909
+ }
910
+ }
911
+
912
+ func cameraDidStopped(kakaoMap: KakaoMap, by: MoveBy) {
913
+ if !isDestroyed {
914
+ onCameraIdle(["reason": reason(by), "camera": cameraPayload(kakaoMap)])
915
+ }
916
+ }
917
+
918
+ func kakaoMapDidTapped(kakaoMap: KakaoMap, point: CGPoint) {
919
+ guard !isDestroyed else { return }
920
+ let coordinate = kakaoMap.getPosition(point).wgsCoord
921
+ onMapClick([
922
+ "latitude": coordinate.latitude,
923
+ "longitude": coordinate.longitude,
924
+ "x": point.x,
925
+ "y": point.y
926
+ ])
927
+ }
928
+
929
+ func terrainDidTapped(kakaoMap: KakaoMap, position: MapPoint) {
930
+ guard !isDestroyed else { return }
931
+ let point = position.wgsCoord
932
+ onTerrainClick(["latitude": point.latitude, "longitude": point.longitude])
933
+ }
934
+
935
+ func kakaoMapFocusDidChanged(kakaoMap: KakaoMap, focus: Bool) {
936
+ guard !isDestroyed else { return }
937
+ onFocusChanged(["focused": focus])
938
+ }
939
+
940
+ func terrainDidLongPressed(kakaoMap: KakaoMap, position: MapPoint) {
941
+ guard !isDestroyed else { return }
942
+ onMapLongClick([
943
+ "latitude": position.wgsCoord.latitude,
944
+ "longitude": position.wgsCoord.longitude
945
+ ])
946
+ }
947
+
948
+ func poiDidTapped(kakaoMap: KakaoMap, layerID: String, poiID: String, position: MapPoint) {
949
+ guard !isDestroyed else { return }
950
+ if labels?.handlesPoiTap(layerId: layerID, id: poiID) == true {
951
+ return
952
+ }
953
+ onPoiClick([
954
+ "latitude": position.wgsCoord.latitude,
955
+ "longitude": position.wgsCoord.longitude,
956
+ "id": poiID,
957
+ "layerId": layerID
958
+ ])
959
+ }
960
+
961
+ func kakaoMapMarginDidUpdated(_ kakaoMap: KakaoMap) {
962
+ guard !isDestroyed else { return }
963
+ onPaddingChanged([
964
+ "top": kakaoMap.margins.top,
965
+ "right": kakaoMap.margins.right,
966
+ "bottom": kakaoMap.margins.bottom,
967
+ "left": kakaoMap.margins.left
968
+ ])
969
+ }
970
+
971
+ func kakaoMapDidResized(_ kakaoMap: KakaoMap) {
972
+ guard !isDestroyed else { return }
973
+ let size = kakaoMap.viewRect.size
974
+ guard lastEmittedViewportSize != size else { return }
975
+ lastEmittedViewportSize = size
976
+ onViewportChanged([
977
+ "x": kakaoMap.viewRect.origin.x,
978
+ "y": kakaoMap.viewRect.origin.y,
979
+ "width": size.width,
980
+ "height": size.height
981
+ ])
982
+ }
983
+
984
+ func compassDidTapped(kakaoMap: KakaoMap) {
985
+ if !isDestroyed {
986
+ onCompassClick([:])
987
+ }
988
+ }
989
+
990
+ func onViewInfoChanged(kakaoMap: KakaoMap, viewInfoName: String) {
991
+ guard !isDestroyed else { return }
992
+ currentViewInfoName = viewInfoName
993
+ onMapTypeChanged(["viewInfoName": viewInfoName])
994
+ }
995
+
996
+ func onViewInfoChangeFailure(kakaoMap: KakaoMap, viewInfoName: String) {
997
+ if !isDestroyed {
998
+ onError([
999
+ "code": "ERR_KAKAO_MAP_VIEW_INFO_CHANGE",
1000
+ "message": "Could not change the map view info to '\(viewInfoName)'."
1001
+ ])
1002
+ }
1003
+ }
1004
+
1005
+ private func reason(_ by: MoveBy) -> String {
1006
+ by == .notUserAction ? "developer" : "gesture"
1007
+ }
1008
+
1009
+ deinit {
1010
+ isDestroyed = true
1011
+ releaseMapObjects()
1012
+ NotificationCenter.default.removeObserver(self)
1013
+ map?.eventDelegate = nil
1014
+ controller?.delegate = nil
1015
+ controller?.pauseEngine()
1016
+ controller?.resetEngine()
1017
+ }
1018
+ }