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¶
- VietMap API key: get from https://maps.vietmap.vn/ (refer to it as
VIETMAP_API_KEY). - Google Maps iOS SDK installed (CocoaPods example):
- Recommended: iOS 15+ and Swift 5.7+.
- Enable Location permission if you want to bias autocomplete results (
focusparameter). - (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
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)¶
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¶
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)¶
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¶
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¶
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
focusto improve relevance. - Always call Place detail before drawing a marker (autocomplete v4 has no coordinates).
- Check for empty
pathsin 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:
app.get('/vm/autocomplete', async (req,res)=>{ /* validate + forward to https://maps.vietmap.vn/api/autocomplete/v4 */ })
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