Today we will discuss implementing google maps features on an app written with swift (an iphone). At launch the app will show the map set to the user’s location and display infos about nearby places in a CollectionView.
Let’ s start:
1- API Key:
you will need to obtain an API key on Google Maps platform by going to the credentials page and then API Key. You might need to enable billing if not done and create a project for your API key.
Your API Key would show as a string that you should copy.
2- PodFile:
- You would need to create a Xcode project (for example GoogleMapSearchPlaces), open a terminal cd to the project directory. Do:
pod init
pod install
In order to create a podfile in your xcode project. 🙂
- For google Maps and Places:
In the podfile type:
source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '13.0'
target 'GoogleMapSearchPlaces' do
pod 'GoogleMaps', '7.3.0'
pod 'GooglePlaces', '7.1.0'
end
3- AppDelegate:
In the AppDelegate.swift file:
import GoogleMaps
import GooglePlaces
in the function:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool
add the following lines for maps and places:
GMSServices.provideAPIKey("YOUR_API_KEY")
GMSPlacesClient.provideAPIKey("YOUR_API_KEY")
return true
4- In the ViewController, we have all the codes, risking a bloated file
🙁
import GoogleMaps
import GooglePlaces
In the ViewController class we add functions and variables to manage the place and the maps:
var locationManager: CLLocationManager!
var currentLocation: CLLocation?
var mapView: GMSMapView!
var placesClient: GMSPlacesClient!
var preciseLocationZoomLevel: Float = 15.0
var approximateLocationZoomLevel: Float = 10.0
var likelyPlaces:[Any] = []
Since we will need a collection to represent the places we also add:
let collectionView: UICollectionView = {
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .horizontal
let cv = UICollectionView(frame: .zero, collectionViewLayout: layout)
cv.translatesAutoresizingMaskIntoConstraints = false
cv.register(PlaceCell.self, forCellWithReuseIdentifier: "cell")
return cv
}()
In the function override func viewDidLoad() we have this for the mapView and collectionview:
// Do any additional setup after loading the view.
// Create a GMSCameraPosition that tells the map to display the
// coordinate -33.86,151.20 at zoom level 6.
// Initialize the location manager.
locationManager = CLLocationManager()
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.requestWhenInUseAuthorization()
locationManager.distanceFilter = 50
locationManager.startUpdatingLocation()
locationManager.delegate = self
placesClient = GMSPlacesClient.shared()
// A default location to use when location permission is not granted.
let defaultLocation = CLLocation(latitude: -33.869405, longitude: 151.199)
// Create a map.
let zoomLevel = locationManager.accuracyAuthorization == .fullAccuracy ? preciseLocationZoomLevel : approximateLocationZoomLevel
let camera = GMSCameraPosition.camera(withLatitude: defaultLocation.coordinate.latitude,
longitude: defaultLocation.coordinate.longitude,
zoom: zoomLevel)
mapView = GMSMapView.map(withFrame: view.bounds, camera: camera)
mapView.settings.myLocationButton = true
mapView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
mapView.isMyLocationEnabled = true
// Add the map to the view, hide it until we've got a location update.
view.addSubview(collectionView)
//vStack.addArrangedSubview(collectionView)
collectionView.backgroundColor = .darkGray
collectionView.delegate = self
collectionView.dataSource = self
collectionView.bottomAnchor.constraint(greaterThanOrEqualTo: view.bottomAnchor, constant: -40).isActive = true
collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 40).isActive = true
collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -40).isActive = true
collectionView.heightAnchor.constraint(equalToConstant: view.frame.width/2).isActive = true
mapView.translatesAutoresizingMaskIntoConstraints = false
mapView.heightAnchor.constraint(equalToConstant: 600).isActive = true
mapView.widthAnchor.constraint(equalToConstant: 400).isActive = true
listLikelyPlaces()
view.addSubview(mapView)
mapView.isHidden = true
We also add an extension to ViewController where the function ListLikelyPlaces() is designed to fetch places, we change this function by having it append the results to likelyPlaces (remember var likelyPlaces:[Any] = [] was defined earlier).
We use a dispatchQueue to reload the collection data. As an exercise try to see what happens if you eliminate the dispacthQueue (and do not remove the codes inside).
extension ViewController: CLLocationManagerDelegate {
// Handle incoming location events.
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
let location: CLLocation = locations.last!
print(“Location: \(location)“)
let zoomLevel = locationManager.accuracyAuthorization == .fullAccuracy ? preciseLocationZoomLevel : approximateLocationZoomLevel
let camera = GMSCameraPosition.camera(withLatitude: location.coordinate.latitude,
longitude: location.coordinate.longitude,
zoom: zoomLevel)
if mapView.isHidden {
mapView.isHidden = false
mapView.camera = camera
} else {
mapView.animate(to: camera)
}
}
// Populate the array with the list of likely places.
func listLikelyPlaces() {
// Clean up from previous sessions.
likelyPlaces.removeAll()
let placeFields: GMSPlaceField = [.name, .coordinate]
placesClient.findPlaceLikelihoodsFromCurrentLocation(withPlaceFields: placeFields) { (placeLikelihoods, error) in
guard error == nil else {
// TODO: Handle the error.
print(“Current Place error: \(error!.localizedDescription)“)
return
}
guard let placeLikelihoods = placeLikelihoods else {
print(“No places found.”)
return
}
//self.Hoods = placeLikelihoods
// Get likely places and add to the list.
for likelihood in placeLikelihoods {
let place = likelihood.place
self.likelyPlaces.append(place)
DispatchQueue.main.async {
self.collectionView.reloadData()
}
//print(“\(place)”)
}
}
}
// Handle authorization for the location manager.
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
// Check accuracy authorization
let accuracy = manager.accuracyAuthorization
switch accuracy {
case .fullAccuracy:
print(“Location accuracy is precise.”)
case .reducedAccuracy:
print(“Location accuracy is not precise.”)
@unknown default:
fatalError()
}
// Handle authorization status
switch status {
case .restricted:
print(“Location access was restricted.”)
case .denied:
print(“User denied access to location.”)
// Display the map using the default location.
mapView.isHidden = false
case .notDetermined:
print(“Location status not determined.”)
case .authorizedAlways: fallthrough
case .authorizedWhenInUse:
print(“Location status is OK.”)
@unknown default:
fatalError()
}
}
// Handle location manager errors.
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
locationManager.stopUpdatingLocation()
print(“Error here: \(error)“)
}
}
We add a new extension to ViewController to take care of our collections:
extension ViewController: UICollectionViewDelegateFlowLayout, UICollectionViewDataSource{
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath)->CGSize{
return CGSize(width: collectionView.frame.width/2.5, height: collectionView.frame.width/2)
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return self.likelyPlaces.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! PlaceCell
cell.backgroundColor = .gray
cell.configure(i: indexPath.row, Arr: self.likelyPlaces)
return cell
}
}
At this point we are disrespecting all architectural principles (MVC, MVVM you name it) we create a new class within ViewController (PlaceCell). This is the UICollectionViewCell. It has a configuring function that takes in an <Int> and an <Array> of <Any> to ‘return’ the text for the places found in the collection.
The PlaceCell contains the UILabels, their text are the places found and in the likelyPlaces array:
class PlaceCell: UICollectionViewCell {
lazy var Img: UILabel = {
let label = UILabel(frame: .zero)
label.translatesAutoresizingMaskIntoConstraints = false
label.numberOfLines = 0
label.textAlignment = .center
label.font = label.font.withSize(12)
label.setContentHuggingPriority(.defaultLow, for: .vertical)
label.setContentCompressionResistancePriority(.defaultLow, for: .vertical)
label.text = “Places”
label.backgroundColor = .systemGray2
return label
}()
func configure( i:Int, Arr:[Any]) {
self.Img.text = “\(Arr[i])“
}
override init(frame: CGRect) {
super.init(frame: .zero)
contentView.addSubview(Img)
Img.topAnchor.constraint(equalTo: contentView.topAnchor).isActive = true
Img.leftAnchor.constraint(equalTo: contentView.leftAnchor).isActive = true
Img.rightAnchor.constraint(equalTo: contentView.rightAnchor).isActive = true
Img.bottomAnchor.constraint(equalTo: contentView.bottomAnchor).isActive = true
}
required init?(coder aDecoder: NSCoder) {
fatalError(“init(coder:) has not been implemented”)
}
}
5- Almost done, Info.plist and locations…
We have all the codes needed or almost. However we need to update the info.plist.
We can click on the highlighted texts and icons of the image. At the bottom of the Custom IOS Target Properties, we click on the “+” in the circle and select Privacy — Location When In Usage Description. Right next to it we double click and write our message, here: “GoogleMapSearchPlaces need …”
We are almost done. Google map does not get your location when it is being used on a simulator, so we need to do the following:
click on the simulator and on the menu tab Features>location>custom location
I have picked Atlanta’s Coordinates.
You should get something like this. Pretty basic…
The github link and complete codes are provided below.
You can improve them by making them more readable (architecture, liskov, decoupling etc) , making the UI nicer or adding features such as a click to select a new location and find new places for example…
Thank you!
github id: Chrisb5a
Codes link: https://github.com/chrisb5a/GoogleMapSearchPlaces/tree/main
if you are interested in learning new skills feel free to reach out.