Apple Core Location in Flutter (using Apple reverseGeocodeLocation to show address details in Flutter app)

Volodymyr Rykhva
4 min readJul 11, 2023

--

Once I needed to use Apple map in Flutter. For a map I simply used Flutter package apple_maps_flutter.

In general it works ok (I won’t compare it with google_maps_flutter as they work flawlessly in Flutter and have a lot of configurations) but the main problem with Apple maps was that Apple maps are a bit useless when I wanted to interact somehow with places depicted on the map. Using google map we won’t have this problem as there is Google Places API for that. But what if I still want to use Apple map? Combining Google Places API or any other API like Yelp, TomTom with Apple map is not an option as there will be a lot of inaccuracies in naming, locations etc.

Then I decided to use Apple Core Location in Flutter app especially its `CLGeocoder().reverseGeocodeLocation(_:completionHandler:)` method to get an address details and show it as an annotation on the map. The good news is that fact that it’s completely free to use.

Let’s dive into coding👨‍💻.

Here is updates to iOS part. All of them are made in AppDelegate file:

import UIKit
import CoreLocation // import CoreLocation
import Flutter

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
createChannelForGettingAddress() // 2) create channel
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}

func createChannelForGettingAddress() {
let controller: FlutterViewController = window?.rootViewController as! FlutterViewController
let addressChannel = FlutterMethodChannel(
name: "volodymyr.rykhva/addressChannel",
binaryMessenger: controller.binaryMessenger)

addressChannel.setMethodCallHandler({ [weak self]
(call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
guard let self = self else {
result(FlutterError(code: "APP_DELEGATE_NOT_FOUND",
message: "App delegate not found",
details: nil))
return
}
switch call.method {
case "getAddress":
guard let args = call.arguments as? [String: Any],
let latitude = args["lat"] as? Double,
let longitude = args["lon"] as? Double else {
result(FlutterError(code: "INVALID_ARGUMENTS",
message: "Invalid arguments",
details: nil))
return
}

self.geocode(latitude: latitude, longitude: longitude) { placemarks, error in
if let error = error {
result(FlutterError(code: "GEOCODING_ERROR",
message: "Error geocoding location",
details: error.localizedDescription))
return
}

guard let placemarks = placemarks,
let firstPlacemark = placemarks.first else {
result(nil)
return
}

let addressDetails: [String: String] = [
"name": firstPlacemark.name ?? "",
"thoroughfare": firstPlacemark.thoroughfare ?? "",
"subThoroughfare": firstPlacemark.subThoroughfare ?? "",
"locality": firstPlacemark.locality ?? "",
"subLocality": firstPlacemark.subLocality ?? "",
"administrativeArea": firstPlacemark.administrativeArea ?? "",
"subAdministrativeArea": firstPlacemark.subAdministrativeArea ?? "",
"postalCode": firstPlacemark.postalCode ?? "",
"country": firstPlacemark.country ?? "",
"isoCountryCode": firstPlacemark.isoCountryCode ?? ""
]

result(addressDetails)
}

default:
result(FlutterMethodNotImplemented)
}
})
}

func geocode(latitude: Double, longitude: Double, completion: @escaping (_ placemark: [CLPlacemark]?, _ error: Error?) -> Void) {
CLGeocoder().reverseGeocodeLocation(CLLocation(latitude: latitude, longitude: longitude), completionHandler: completion)
}
}

What about Flutter updates?:

  1. Create addressChannel (note that its name should exactly match to the name in iOS part):
static const addressChannel =
MethodChannel('volodymyr.rykhva/addressChannel');

2. Create _getAddress(lat, lon) method which aim is to return PlaceDetails object with all detailed information (NOTE: name getAddress should exactly match function name in iOS part):

  Future<PlaceDetails> _getAddress(double lat, double lon) async {
final args = {
'lat': lat,
'lon': lon,
};
dynamic result = await addressChannel.invokeMethod('getAddress', args);
return PlaceDetails.fromJson(result.cast<String, dynamic>());
}

3. PlaceDetails class holds all information which we can get from Apple CLGeocoder:

class PlaceDetails {
String? administrativeArea;
String? postalCode;
String? subThoroughfare;
String? country;
String? name;
String? subAdministrativeArea;
String? subLocality;
String? thoroughfare;
String? locality;
String? isoCountryCode;

PlaceDetails({
this.administrativeArea,
this.postalCode,
this.subThoroughfare,
this.country,
this.name,
this.subAdministrativeArea,
this.subLocality,
this.thoroughfare,
this.locality,
this.isoCountryCode,
});

factory PlaceDetails.fromJson(Map<String, dynamic> json) {
return PlaceDetails(
administrativeArea: json['administrativeArea'],
postalCode: json['postalCode'],
subThoroughfare: json['subThoroughfare'],
country: json['country'],
name: json['name'],
subAdministrativeArea: json['subAdministrativeArea'],
subLocality: json['subLocality'],
thoroughfare: json['thoroughfare'],
locality: json['locality'],
isoCountryCode: json['isoCountryCode'],
);
}

@override
String toString() {
return 'PlaceDetails{administrativeArea: $administrativeArea, postalCode: $postalCode, subThoroughfare: $subThoroughfare, country: $country, name: $name, subAdministrativeArea: $subAdministrativeArea, subLocality: $subLocality, thoroughfare: $thoroughfare, locality: $locality, isoCountryCode: $isoCountryCode}';
}
}

4. Tapping anywhere on a map calls onTap callback. And we implement it in the following way:

  void _onTap(LatLng latLng) async {
final res = await _getAddress(latLng.latitude, latLng.longitude);
final id = AnnotationId(Random().nextInt(1000000).toString());
final annotation = Annotation(
annotationId: id,
position: latLng,
infoWindow: InfoWindow(
title: res.name,
),
);
setState(() {
annotations = {annotation};
annotationDetails = res;
});
Future.delayed(
const Duration(milliseconds: 100),
() => mapController.showMarkerInfoWindow(id),
);
}

Here is a full code of `main.dart`:

import 'dart:math';

import 'package:apple_maps_flutter/apple_maps_flutter.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

import 'place_details.dart';

void main() {
runApp(const MyApp());
}

class MyApp extends StatelessWidget {
const MyApp({super.key});

@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const MyHomePage(),
);
}
}

class MyHomePage extends StatefulWidget {
const MyHomePage({
super.key,
});

@override
State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
static const addressChannel =
MethodChannel('volodymyr.rykhva/addressChannel');
late AppleMapController mapController;
Set<Annotation> annotations = {};
PlaceDetails? annotationDetails;

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: const Text('Method channel test'),
),
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Expanded(
child: AppleMap(
annotations: annotations,
onMapCreated: _onMapCreated,
onTap: _onTap,
initialCameraPosition: const CameraPosition(
target: LatLng(51.509865, -0.118092),
zoom: 15,
),
),
),
const Divider(
height: 1,
),
if (annotationDetails != null)
SizedBox(
height: 150,
child: Padding(
padding: const EdgeInsets.all(10.0),
child: Text(
annotationDetails.toString(),
),
),
),
],
),
);
}

Future<PlaceDetails> _getAddress(double lat, double lon) async {
final args = {
'lat': lat,
'lon': lon,
};
dynamic result = await addressChannel.invokeMethod('getAddress', args);
return PlaceDetails.fromJson(result.cast<String, dynamic>());
}

void _onMapCreated(AppleMapController controller) {
mapController = controller;
}

void _onTap(LatLng latLng) async {
final res = await _getAddress(latLng.latitude, latLng.longitude);
final id = AnnotationId(Random().nextInt(1000000).toString());
final annotation = Annotation(
annotationId: id,
position: latLng,
infoWindow: InfoWindow(
title: res.name,
),
);
setState(() {
annotations = {annotation};
annotationDetails = res;
});
Future.delayed(
const Duration(milliseconds: 100),
() => mapController.showMarkerInfoWindow(id),
);
}
}

If you want to have a look on a full working project — here is a link on a Github repository: https://github.com/ascentman/apple-core-location-in-flutter 😇 Happy coding!🙌

--

--

No responses yet