Интеграция Google Maps SDK в мобильное приложение
Google Maps SDK для Android и iOS — зрелый инструмент, но у него есть нюансы, которые регулярно ломают первые интеграции: неправильная инициализация API key, несовместимость версий с Gradle / Swift Package Manager, и отображение серой карты без понятного сообщения об ошибке в логах.
Первый запуск: API key и ограничения
API key создаётся в Google Cloud Console, проект должен включать Maps SDK for Android и Maps SDK for iOS как отдельные продукты. Ключ нужно ограничить: для Android — по applicationId (SHA-1 fingerprint + package name), для iOS — по Bundle ID. Без ограничений ключ можно вытащить из APK декомпилятором за 5 минут.
На Android ключ прописывается в AndroidManifest.xml:
<meta-data
android:name="com.google.android.geo.API_KEY"
android:value="${MAPS_API_KEY}" />
Значение MAPS_API_KEY задаётся в local.properties и подставляется через buildConfigField в build.gradle — не хардкодить строку прямо в манифест.
На iOS — в AppDelegate или через GMSServices.provideAPIKey() до создания любого GMSMapView:
import GoogleMaps
@main
struct AppEntry: App {
init() {
GMSServices.provideAPIKey("YOUR_API_KEY")
}
var body: some Scene {
WindowGroup { ContentView() }
}
}
Серая карта — что проверить
- API key не имеет прав на нужный продукт (Maps SDK vs Maps JavaScript API — разные продукты)
- Billing не подключён к проекту в GCP (Maps SDK требует активного биллинга, даже в рамках бесплатного лимита)
- На Android —
minSdkVersionниже 21 или нет зависимостиcom.google.android.gms:play-services-maps - На iOS — файл
GoogleMaps.xcframeworkне добавлен вFrameworks, Libraries, and Embedded Content
Android: базовая настройка MapView
// build.gradle (app)
implementation("com.google.android.gms:play-services-maps:18.2.0")
// Fragment
class MapFragment : Fragment(), OnMapReadyCallback {
private lateinit var map: GoogleMap
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val mapFragment = childFragmentManager
.findFragmentById(R.id.map) as SupportMapFragment
mapFragment.getMapAsync(this)
}
override fun onMapReady(googleMap: GoogleMap) {
map = googleMap
map.uiSettings.isZoomControlsEnabled = true
map.moveCamera(
CameraUpdateFactory.newLatLngZoom(
LatLng(55.7558, 37.6173), // Москва
12f
)
)
}
}
SupportMapFragment предпочтительнее MapView — он сам управляет lifecycle (onCreate, onResume, onPause, onDestroy). Если используете MapView напрямую — каждый lifecycle-метод нужно пробрасывать вручную, и забытый mapView.onDestroy() приводит к утечке памяти.
iOS: SwiftUI и UIKit
В UIKit — GMSMapView добавляется как обычный UIView. В SwiftUI — оборачиваем через UIViewRepresentable:
struct GoogleMapView: UIViewRepresentable {
let coordinate: CLLocationCoordinate2D
let zoom: Float
func makeUIView(context: Context) -> GMSMapView {
let camera = GMSCameraPosition(target: coordinate, zoom: zoom)
let mapView = GMSMapView(frame: .zero, camera: camera)
mapView.isMyLocationEnabled = true
return mapView
}
func updateUIView(_ mapView: GMSMapView, context: Context) {
let camera = GMSCameraPosition(target: coordinate, zoom: zoom)
mapView.animate(to: camera)
}
}
Кастомные стили карты
Google Maps поддерживает JSON-стили через GMSMapStyle (iOS) и MapStyleOptions (Android). Стили генерируются в Google Maps Platform Styling Wizard. Применяются одной строкой:
mapView.mapStyle = try? GMSMapStyle(jsonString: mapStyleJSON)
Типичные проблемы при интеграции
Android: ClassNotFoundException: com.google.android.gms.maps.MapFragment — устаревший MapFragment вместо SupportMapFragment. В современных проектах с Navigation Component всегда используйте SupportMapFragment.
iOS: краш GMSServices.provideAPIKey вызван дважды — если AppDelegate и SceneDelegate оба инициализируют SDK. Вынесите инициализацию в одно место.
Квоты: бесплатный лимит Google Maps — 28 500 загрузок карты в месяц. Каждый getMapAsync или создание GMSMapView — это одна загрузка. Если карта пересоздаётся при каждом открытии экрана (неправильный lifecycle), квота тает быстро.
Сроки
1–3 дня. Базовая карта с маркерами — 1 день. Полная интеграция с кастомным стилем, инфо-окнами и маршрутами — 2–3 дня. Стоимость рассчитывается индивидуально.







