Source: https://maps.vietmap.vn/docs/sdk-mobile/vietmap-google-sdk/ios/ # Integrating VietMap APIs inside a Google Maps iOS app (Direct HTTP) Requirement: Use Google Maps SDK only for base map rendering, while ALL search (autocomplete), place detail, and routing data come from VietMap APIs (no Google Places calls). This document shows how to call VietMap REST endpoints directly with `URLSession`. > If later you need vector tile rendering, navigation or traffic layers from VietMap itself, consider migrating to the native VietMap maps/navigation SDK. This guide keeps Google base map but 100% VietMap data. ## 1. Prerequisites 1. VietMap API key: get from https://maps.vietmap.vn/ (refer to it as `VIETMAP_API_KEY`). 2. Google Maps iOS SDK installed (CocoaPods example): ```ruby pod 'GoogleMaps' ``` 3. Recommended: iOS 15+ and Swift 5.7+. 4. Enable Location permission if you want to bias autocomplete results (`focus` parameter). 5. (Recommended) Backend proxy to protect the key (see section 10). Current requirement allows direct calls, but in production prefer a proxy. ## 2. VietMap endpoints used in this guide | Purpose | Method & Path | Notes | |---------|---------------|-------| | Autocomplete / Merged Search v4 | `GET /api/autocomplete/v4` | Returns suggestions list (NO coordinates) | | Place Detail v4 | `GET /api/place/v4` | Use `refid` to obtain lat/lng & address components | | Route v4 | `GET /api/route/v4` | Returns paths (encoded polyline) | Base host: `https://maps.vietmap.vn` ## 3. Query examples (raw) Autocomplete v4 (biased with user location): ``` GET https://maps.vietmap.vn/api/autocomplete/v4?apikey=YOUR_KEY&text=ho chi minh&focus=10.7769,106.7008&display_type=1 ``` Place detail v4: ``` GET https://maps.vietmap.vn/api/place/v4?apikey=YOUR_KEY&refid=REF_ID ``` Route (2 points): ``` GET https://maps.vietmap.vn/api/route/v4?apikey=YOUR_KEY&vehicle=car&points_encoded=true&point=10.7769,106.7008&point=10.7801,106.7102 ``` ## 4. Swift data models (Codable) ```swift import Foundation import CoreLocation // MARK: - Autocomplete / Search Item (v4) struct VMSearchItem: Codable, Identifiable { let refId: String let distance: Double? let address: String? let name: String? let display: String? let boundaries: [VMBoundary]? let categories: [String]? let entryPoints: [VMEntryPoint]? let dataOld: VMSearchVariant? let dataNew: VMSearchVariant? var id: String { refId } enum CodingKeys: String, CodingKey { case refId = "ref_id" case distance, address, name, display, boundaries, categories case entryPoints = "entry_points" case dataOld = "data_old" case dataNew = "data_new" } } struct VMBoundary: Codable { let type: Int? let id: Int? let name: String? let prefix: String? let fullName: String? enum CodingKeys: String, CodingKey { case type, id, name, prefix; case fullName = "full_name" } } struct VMEntryPoint: Codable { let refId: String?; let name: String?; enum CodingKeys: String, CodingKey { case refId = "ref_id"; case name } } struct VMSearchVariant: Codable { let refId: String? let distance: Double? let address: String? let name: String? let display: String? let boundaries: [VMBoundary]? let categories: [String]? let entryPoints: [VMEntryPoint]? enum CodingKeys: String, CodingKey { case refId = "ref_id", distance, address, name, display, boundaries, categories; case entryPoints = "entry_points" } } // MARK: - Place Detail (v4) struct VMPlaceDetail: Codable { let display: String? let name: String? let hsNum: String? let street: String? let address: String? let cityId: Int? let city: String? let districtId: Int? let district: String? let wardId: Int? let ward: String? let lat: Double? let lng: Double? enum CodingKeys: String, CodingKey { case display, name, street, address, city, district, ward, lat, lng case hsNum = "hs_num" case cityId = "city_id" case districtId = "district_id" case wardId = "ward_id" } } // MARK: - Route (v4) response (minimal) struct VMRouteResponse: Codable { let paths: [VMRoutePath]? } struct VMRoutePath: Codable { let distance: Double; let time: Double?; let points: String } ``` ## 5. Networking layer ```swift import Foundation import UIKit import GoogleMaps import CoreLocation final class VietMapAPIClient { static let shared = VietMapAPIClient() private init() {} private let host = URL(string: "https://maps.vietmap.vn")! private let apiKey = "YOUR_API_KEY_HERE" // TODO: inject securely / runtime private let session = URLSession(configuration: .default) enum VMError: Error { case invalidURL, http(Int, Data), decode(Error), empty, other(Error) } // Common decoder private let decoder: JSONDecoder = { let d = JSONDecoder(); return d }() // Autocomplete/Search v4 @available(iOS 16.0, *) func autocomplete(text: String, focus: CLLocationCoordinate2D? = nil, displayType: Int = 1) async throws -> [VMSearchItem] { var comps = URLComponents(url: host.appending(path: "/api/autocomplete/v4"), resolvingAgainstBaseURL: false)! var q: [URLQueryItem] = [ .init(name: "apikey", value: apiKey), .init(name: "text", value: text), .init(name: "display_type", value: String(displayType)) ] if let focus = focus { q.append(.init(name: "focus", value: String(format: "%.6f,%.6f", focus.latitude, focus.longitude))) } comps.queryItems = q guard let url = comps.url else { throw VMError.invalidURL } let (data, resp) = try await session.data(from: url) guard let http = resp as? HTTPURLResponse else { throw VMError.empty } guard http.statusCode == 200 else { throw VMError.http(http.statusCode, data) } let arr = try decoder.decode([VMSearchItem].self, from: data) return arr } // Place detail v4 @available(iOS 16.0, *) func placeDetail(refId: String) async throws -> VMPlaceDetail { var comps = URLComponents(url: host.appending(path: "/api/place/v4"), resolvingAgainstBaseURL: false)! comps.queryItems = [.init(name: "apikey", value: apiKey), .init(name: "refid", value: refId)] guard let url = comps.url else { throw VMError.invalidURL } let (data, resp) = try await session.data(from: url) guard let http = resp as? HTTPURLResponse, http.statusCode == 200 else { throw VMError.http((resp as? HTTPURLResponse)?.statusCode ?? -1, data) } return try decoder.decode(VMPlaceDetail.self, from: data) } // Route v4 func route(points: [CLLocationCoordinate2D], vehicle: String = "car", pointsEncoded: Bool = true) async throws -> VMRouteResponse { precondition(points.count >= 2, "Need at least 2 points") var base = "https://maps.vietmap.vn/api/route/v4?apikey=\(apiKey)&vehicle=\(vehicle)&points_encoded=\(pointsEncoded)" for p in points { base += "&point=\(p.latitude),\(p.longitude)" } guard let url = URL(string: base) else { throw VMError.invalidURL } let (data, resp) = try await session.data(from: url) guard let http = resp as? HTTPURLResponse, http.statusCode == 200 else { throw VMError.http((resp as? HTTPURLResponse)?.statusCode ?? -1, data) } return try decoder.decode(VMRouteResponse.self, from: data) } } ``` ## 6. Polyline decode helper (precision 1e5) ```swift import CoreLocation func decodePolyline(_ encoded: String) -> [CLLocationCoordinate2D] { var coords: [CLLocationCoordinate2D] = [] var index = encoded.startIndex var lat: Int = 0 var lng: Int = 0 while index < encoded.endIndex { var result = 0 var shift = 0 var byte: Int repeat { byte = Int(encoded[index].asciiValue! - 63) index = encoded.index(after: index) result |= (byte & 0x1F) << shift shift += 5 } while byte >= 0x20 && index < encoded.endIndex let dlat = (result & 1) != 0 ? ~(result >> 1) : (result >> 1) lat += dlat result = 0 shift = 0 repeat { byte = Int(encoded[index].asciiValue! - 63) index = encoded.index(after: index) result |= (byte & 0x1F) << shift shift += 5 } while byte >= 0x20 && index < encoded.endIndex let dlng = (result & 1) != 0 ? ~(result >> 1) : (result >> 1) lng += dlng let finalLat = Double(lat) / 1e5 let finalLng = Double(lng) / 1e5 coords.append(CLLocationCoordinate2D(latitude: finalLat, longitude: finalLng)) } return coords } ``` ## 7. ViewController integration example ```swift import UIKit import GoogleMaps import CoreLocation @MainActor final class VietMapGoogleVC: UIViewController, UITableViewDataSource, UITableViewDelegate, CLLocationManagerDelegate, UITextFieldDelegate { @IBOutlet weak var mapContainer: UIView! @IBOutlet weak var tableView: UITableView! @IBOutlet weak var searchField: UITextField! @IBOutlet weak var fromToLabel: UILabel! @IBOutlet weak var searchButton: UIButton! private var mapView: GMSMapView! private var suggestions: [VMSearchItem] = [] private var fromItem: VMPlaceDetail? private var toItem: VMPlaceDetail? private let locationManager = CLLocationManager() private var focusCoord: CLLocationCoordinate2D? private var fromMarker: GMSMarker? private var toMarker: GMSMarker? private var routePolyline: GMSPolyline? private var isLoading = false { didSet { updateSearchButtonState() } } override func viewDidLoad() { super.viewDidLoad() tableView.dataSource = self tableView.delegate = self searchField.delegate = self let camera = GMSCameraPosition.camera(withLatitude: 10.776889, longitude: 106.700806, zoom: 12) mapView = GMSMapView(frame: mapContainer.bounds, camera: camera) mapView.autoresizingMask = [.flexibleWidth, .flexibleHeight] mapContainer.addSubview(mapView) mapView.isMyLocationEnabled = true locationManager.delegate = self locationManager.requestWhenInUseAuthorization() updateFromToLabel() updateSearchButtonState() updateSearchPlaceholder() updateSuggestionsUI() } @available(iOS 16.0, *) @IBAction func onSearchTap() { Task { await performSearch() } } @available(iOS 16.0, *) private func performSearch() async { guard let q = searchField.text?.trimmingCharacters(in: .whitespacesAndNewlines), !q.isEmpty else { return } isLoading = true do { let items = try await VietMapAPIClient.shared.autocomplete(text: q, focus: focusCoord) suggestions = items tableView.reloadData() updateSuggestionsUI() } catch { showAlert("Search error: \(error.localizedDescription)") } isLoading = false updateSuggestionsUI() } @available(iOS 16.0, *) private func selectSuggestion(_ item: VMSearchItem) async { do { let detail = try await VietMapAPIClient.shared.placeDetail(refId: item.refId) if fromItem == nil { fromItem = detail focusOn(detail) searchField.text = "" } else if toItem == nil { toItem = detail searchField.text = "" } else { clearSelection() fromItem = detail focusOn(detail) searchField.text = "" } renderMarkers() updateFromToLabel() if fromItem != nil, toItem == nil { focusOn(detail) } if fromItem != nil, toItem != nil { await buildRoute() suggestions.removeAll() tableView.reloadData() updateSuggestionsUI() } } catch { showAlert("Place error: \(error.localizedDescription)") } } private func buildRoute() async { guard let f = fromItem, let t = toItem, let flat = f.lat, let flng = f.lng, let tlat = t.lat, let tlng = t.lng else { return } do { searchField.resignFirstResponder() let resp = try await VietMapAPIClient.shared.route(points: [CLLocationCoordinate2D(latitude: flat, longitude: flng), CLLocationCoordinate2D(latitude: tlat, longitude: tlng)]) guard let pathObj = resp.paths?.first else { showAlert("No path"); return } let coords = decodePolyline(pathObj.points) drawPolyline(coords) fitBounds(coords) } catch { showAlert("Route error: \(error.localizedDescription)") } } private func renderMarkers() { if let f = fromItem, let lat = f.lat, let lng = f.lng { if fromMarker == nil { fromMarker = makeMarker(title: "From", color: .systemBlue) } fromMarker?.position = CLLocationCoordinate2D(latitude: lat, longitude: lng) fromMarker?.map = mapView } else { fromMarker?.map = nil fromMarker = nil } if let t = toItem, let lat = t.lat, let lng = t.lng { if toMarker == nil { toMarker = makeMarker(title: "To", color: .systemRed) } toMarker?.position = CLLocationCoordinate2D(latitude: lat, longitude: lng) toMarker?.map = mapView } else { toMarker?.map = nil toMarker = nil } } private func makeMarker(title: String, color: UIColor) -> GMSMarker { let marker = GMSMarker() marker.title = title marker.icon = GMSMarker.markerImage(with: color) marker.map = mapView return marker } private func drawPolyline(_ coords: [CLLocationCoordinate2D]) { routePolyline?.map = nil guard !coords.isEmpty else { return } let path = GMSMutablePath() coords.forEach { path.add($0) } let line = GMSPolyline(path: path) line.strokeColor = .systemBlue line.strokeWidth = 4 line.map = mapView routePolyline = line } private func fitBounds(_ coords: [CLLocationCoordinate2D]) { guard !coords.isEmpty else { return } var bounds = GMSCoordinateBounds() coords.forEach { bounds = bounds.includingCoordinate($0) } mapView.animate(with: GMSCameraUpdate.fit(bounds, withPadding: 48)) } private func clearSelection() { fromItem = nil toItem = nil fromMarker?.map = nil toMarker?.map = nil routePolyline?.map = nil fromMarker = nil toMarker = nil routePolyline = nil searchField.text = "" updateFromToLabel() updateSuggestionsUI() } private func updateFromToLabel() { fromToLabel.text = "From: \(fromItem?.display ?? "-") | To: \(toItem?.display ?? "-")" updateSearchPlaceholder() } private func updateSearchButtonState() { searchButton.isEnabled = !isLoading searchButton.alpha = isLoading ? 0.5 : 1 } private func updateSearchPlaceholder() { if fromItem == nil { searchField.placeholder = "Search starting point" } else if toItem == nil { searchField.placeholder = "Search destination" } else { searchField.placeholder = "Search places" } } private func updateSuggestionsUI() { tableView.isHidden = suggestions.isEmpty } private func focusOn(_ detail: VMPlaceDetail, zoom: Float = 15) { guard let lat = detail.lat, let lng = detail.lng else { return } let camera = GMSCameraUpdate.setTarget(CLLocationCoordinate2D(latitude: lat, longitude: lng), zoom: zoom) mapView.animate(with: camera) } private func showAlert(_ message: String) { let alert = UIAlertController(title: "Info", message: message, preferredStyle: .alert) alert.addAction(UIAlertAction(title: "OK", style: .default)) present(alert, animated: true) } // MARK: TableView func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { suggestions.count } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "cell") ?? UITableViewCell(style: .subtitle, reuseIdentifier: "cell") let s = suggestions[indexPath.row] cell.textLabel?.text = s.display?.isEmpty == false ? s.display : s.name cell.detailTextLabel?.text = s.address return cell } func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { tableView.deselectRow(at: indexPath, animated: true) if #available(iOS 16.0, *) { let item = suggestions[indexPath.row] Task { await selectSuggestion(item) } } else { // Fallback on earlier versions } } // MARK: Location bias (optional) func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) { if manager.authorizationStatus == .authorizedWhenInUse || manager.authorizationStatus == .authorizedAlways { manager.startUpdatingLocation() } } func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { focusCoord = locations.last?.coordinate } // MARK: Text Field func textFieldShouldReturn(_ textField: UITextField) -> Bool { if #available(iOS 16.0, *) { Task { await performSearch() } } textField.resignFirstResponder() return true } } ``` ## 8. Basic error handling pattern ```swift Task { do { let items = try await VietMapAPIClient.shared.autocomplete(text: query, focus: focusCoord) // update UI } catch VietMapAPIClient.VMError.http(let code, let body) { print("HTTP error: \(code) body=\(String(data: body, encoding: .utf8) ?? "?")") } catch { print("Unexpected: \(error)") } } ``` ## 9. Performance & UX tips * Debounce typing (>=300 ms) before firing autocomplete. * Temporarily cache suggestions by (text + rounded focus) for 30–60 s to reduce quota. * If user moves > ~500 m, refresh `focus` to improve relevance. * Always call Place detail before drawing a marker (autocomplete v4 has no coordinates). * Check for empty `paths` in routing responses. * Avoid re-requesting a route if both endpoints are unchanged. ## 10. (Optional) Backend proxy (recommended for production) Benefits: Hide API key, enforce rate limiting, central logging, lightweight caching. Although this example calls APIs directly, production apps should proxy. Pseudo Node.js: ```js app.get('/vm/autocomplete', async (req,res)=>{ /* validate + forward to https://maps.vietmap.vn/api/autocomplete/v4 */ }) ``` The client only talks to your domain; the server appends the VietMap `apikey`. ## 11. Quick verification checklist | Step | Expect | |------|--------| | Autocomplete "ho chi" | List of `VMSearchItem` (no lat/lng) | | Place detail for first row | lat/lng + display available | | Select second point | Blue polyline drawn | | Reset action | Markers & polyline cleared | ## 12. Conclusion You can enrich an existing Google Maps iOS app with VietMap geographic data (search, place detail, routing) using just a few HTTP calls. When you need advanced features (turn‑by‑turn navigation, vector styling, traffic), consider the native VietMap SDK. --- © VietMap