Skip to content

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):
    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)

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 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.

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 */ })
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

facebook
Tổng đài hỗ trợ
089.616.4567
facebook Chat Facebook zalo Chat Zalo