Using Google Places API to find nearby places BY TYPE (parks) for iOS SwiftUI - ios

I'm pretty new to Swift/iOS app dev. so far, so I'm struggling to figure this out. Basically, I'm trying to make it so when the app opens (first screen is the map), it automatically finds nearby places that are only parks around the user's current location and have these locations annotated with markers on a map (Google Maps) using Google Places API for iOS using updated SwiftUI/Swift 5.0: Using no storyboards!. Table I types, in this case, parks: https://developers.google.com/places/web-service/supported_types
So far, this is the code I have... It uses GMSPlaceLikelihood of places nearby the user's location. This is more so an example of what I want to achieve, however using Google Places API nearby search instead so I can show only parks. Image of app running:
Image
(The place's found from nearby are then listed in a table as shown on the image. This list is just for show)
Thanks in advance for any advice/help.
GoogleMapsView.swift:
#ObservedObject var locationManager = LocationManager()
#ObservedObject var place = PlacesManager()
func makeUIView(context: Self.Context) -> GMSMapView {
let camera = GMSCameraPosition.camera(withLatitude: locationManager.latitude, longitude: locationManager.longitude, zoom: 14)
let mapView = GMSMapView.map(withFrame: CGRect.zero, camera: camera)
mapView.isMyLocationEnabled = true
mapView.settings.rotateGestures = false
mapView.settings.tiltGestures = false
mapView.isIndoorEnabled = false
mapView.isTrafficEnabled = false
mapView.isBuildingsEnabled = false
mapView.settings.myLocationButton = true
place.currentPlacesList(completion: { placeLikelihoodList in
if let placeLikelihoodList = placeLikelihoodList {
print("total places: \(placeLikelihoodList.count)")
for likelihood in placeLikelihoodList {
let place = likelihood.place
let position = CLLocationCoordinate2D(latitude: place.coordinate.latitude, longitude: place.coordinate.longitude)
let marker = GMSMarker(position: position)
marker.title = place.name
marker.map = mapView
}
}
})
return mapView
}
func updateUIView(_ mapView: GMSMapView, context: Context) {
// let camera = GMSCameraPosition.camera(withLatitude: locationManager.latitude, longitude: locationManager.longitude, zoom: zoom)
// mapView.camera = camera
mapView.animate(toLocation: CLLocationCoordinate2D(latitude: locationManager.latitude, longitude: locationManager.longitude))
}
PlacesManager.swift:
class PlacesManager: NSObject, ObservableObject {
private var placesClient = GMSPlacesClient.shared()
#Published var places = [GMSPlaceLikelihood]()
override init() {
super.init()
currentPlacesList { (places) in
guard let places = places else {
return
}
self.places = places
}
}
func currentPlacesList(completion: #escaping (([GMSPlaceLikelihood]?) -> Void)) {
// Specify the place data types to return.
let fields: GMSPlaceField = GMSPlaceField(rawValue: UInt(GMSPlaceField.name.rawValue) |
UInt(GMSPlaceField.placeID.rawValue) | UInt(GMSPlaceField.types.rawValue) | UInt(GMSPlaceField.coordinate.rawValue))!
placesClient.findPlaceLikelihoodsFromCurrentLocation(withPlaceFields: fields, callback: {
(placeLikelihoodList: Array<GMSPlaceLikelihood>?, error: Error?) in
if let error = error {
print("An error occurred: \(error.localizedDescription)")
return
}
if let placeLikelihoodList = placeLikelihoodList {
for likelihood in placeLikelihoodList {
let place = likelihood.place
}
completion(placeLikelihoodList)
}
})
}
ContentView.swift:
var body: some View {
VStack {
GoogleMapsView()
.edgesIgnoringSafeArea(.top)
.frame(height: 400)
PlacesList()
}
.offset(y: 100)
}

https://developers.google.com/places/web-service/search
I suggest you visit this page, it describes the API in detail and shows examples how to call it with different parameters.
After you get the response (I suggest getting it in json format and using SwiftyJSON cocoa pod to parse it) populate the table.

Related

Why GMUClusterManager is not showing on iOS?

I try to use SwiftUI view using the UIViewRepresentable pattern, the GoogleMaps background works but the ClusterManager doesn't
show anything. Is there something wrong?
I try to generate 10000 markers like in the sample of Google Maps documentation, but instead of using UIKit with "UIViewController" I try to use SwiftUI with UIViewRepresentable just like they did there. It works if I just use markers but if I try to use ClusterManager it doesn't work.
import SwiftUI
import GoogleMaps
import GoogleMapsUtils
struct GoogleMapsView: UIViewRepresentable {
let kClusterItemCount = 10000
let kCameraLatitude = 48.860294
let kCameraLongitude = 2.338629
func makeUIView(context: Self.Context) -> GMSMapView {
print("-- GoogleMapsView -- : Making UiView MapView")
let camera = GMSCameraPosition.camera(withLatitude: kCameraLatitude,
longitude: kCameraLongitude, zoom: 10)
let mapView = GMSMapView.map(withFrame: CGRect.zero, camera: camera)
return mapView
}
func makeCoordinator() -> MapCoordinator {
print("-- GoogleMapsView -- : Making Coordinator")
return MapCoordinator(owner: self)
}
func updateUIView(_ mapView: GMSMapView, context: Context) {
print("-- GoogleMapsView -- : Updating UI View")
let iconGenerator = GMUDefaultClusterIconGenerator()
let algorithm = GMUNonHierarchicalDistanceBasedAlgorithm()
let renderer = GMUDefaultClusterRenderer(mapView: mapView, clusterIconGenerator: iconGenerator)
let clusterManager = GMUClusterManager(map: mapView, algorithm: algorithm, renderer: renderer)
generateClusterItems(clusterManager: clusterManager)
clusterManager.cluster()
}
private func generateClusterItems(clusterManager : GMUClusterManager) {
let extent = 0.2
for _ in 1...kClusterItemCount {
let lat = kCameraLatitude + extent * randomScale()
let lng = kCameraLongitude + extent * randomScale()
let position = CLLocationCoordinate2D(latitude: lat, longitude: lng)
let marker = GMSMarker(position: position)
clusterManager.add(marker)
}
}
/// Returns a random value between -1.0 and 1.0.
private func randomScale() -> Double {
return Double(arc4random()) / Double(UINT32_MAX) * 2.0 - 1.0
}
}
I just got this.
You are creating clusterManager inside updateUIView but not saving it anywhere, so it’s immediately discarded. In the Google sample code, they save clusterManager as a property of the view controller; you don’t have a view controller, so I’d recommend moving clusterManager and related code into your MapCoordinator class. Coordinators are kept around for the lifetime of your view and can be accessed from makeUIView and updateUIView using context.coordinator.

mapView: GMSMapView! nil causing the app to crash (swift)

I'm completely new when it comes to use Google Maps SKD for iOS and I have the following problem: My app uses Firebase and the MapVC is the first VC if the user is logged in. I set a UIVIew in the storyboard with the size I wanted and made its class GMSMapView. I also use the CLLocation Manager and the location comes ok, I pass it on to the camera variable and it's fine. But if add this code: mapView = GMSMapView.map(withFrame: CGRect.zero, camera: camera) or this code: self.mapView.camera = camera the app crashes because mapView is nil. If I don't add those lines of code, the map shown is just the default map (Europe). What I need is the map camera to show the location I'm getting from CLLocation and updates it on the map as the location changes as well.
My code for the MapVC is below:
import UIKit
import GoogleMaps
import GooglePlaces
import CoreLocation
import Alamofire
import Firebase
import FirebaseFirestore
class AlertaVC: UIViewController, CLLocationManagerDelegate {
#IBOutlet weak var alertaBtn: RoundButton!
var latitude: CLLocationSpeed?
var longitude: CLLocationDegrees?
var mapView: GoogleMaps.GMSMapView!
var db: Firestore!
var userID: String?
var nameUser: Any?
let locationManager = CLLocationManager()
#IBAction func dispararAlertaBtn(_ sender: UIButton) {
//changing the title and color after the alert is sent
alertaBtn.isSelected = !alertaBtn.isSelected
if alertaBtn.isSelected {
//setting up twilio to send the alert SMS
let headers: HTTPHeaders = [
"Content-Type": "application/x-www-form-urlencoded"
]
//add the phone numbers from the firebase document
let parameters: Parameters = [
"To": "phone-number",
"Body": "Ola, to enviando SMS!"
]
//change the SMS body to include the user's name and the link with live location
AF.request("https://url.twil.io/smsAlerta", method: .post, parameters: parameters, headers: headers).responseJSON { response in
print(response.response as Any, "response alamofire")
}
alertaBtn.setTitle("Encerrar alerta!", for: .normal)
alertaBtn.backgroundColor = UIColor(red: 0.83529, green: 0.4, blue: 0.5725490196, alpha: 1.0)
} else {
alertaBtn.setTitle("Disparar alerta!", for: .normal)
alertaBtn.backgroundColor = UIColor(red: 0.3254, green: 0.1921, blue: 0.2627, alpha: 1.0)
}
}
override func viewDidLoad() {
super.viewDidLoad()
locationManager.requestAlwaysAuthorization()
locationManager.requestWhenInUseAuthorization()
if CLLocationManager.locationServicesEnabled() {
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters
locationManager.startUpdatingLocation()
}
db = Firestore.firestore()
let user = Auth.auth().currentUser
if let user = user {
// The user's ID, unique to the Firebase project.
self.userID = user.uid
print(self.userID!, "user ID firebase")
}
}
override func viewWillAppear(_ animated: Bool) {
//showCurrentLocation() Tried calling the function #ViewWillAppear but it didn't work either
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let locValue: CLLocationCoordinate2D = manager.location?.coordinate else { return }
latitude = locValue.latitude
longitude = locValue.longitude
let camera: GMSCameraPosition = GMSCameraPosition.camera(withLatitude: latitude ?? 19.741755, longitude: longitude ?? -155.844437, zoom: 7.0) //if doesnt load will show Hawaii
print(camera as Any, "camera") <-- WORKS FINE: GMSCameraPosition 0x600000f389f0: target:(37.332, -122.031) bearing:0.000 zoomLevel:7.000 viewingAngle:0.000 camera
self.mapView.camera = camera <<--- CRASHES HERE: Fatal error: Unexpectedly found nil while implicitly unwrapping an Optional value
self.mapView?.animate(to: camera) <<-- WITHOUT THE LINE ABOVE THIS ANIMATION DOESN`T HAPPEN, BECAUSE MAPVIEW IS NIL
print("locations = \(latitude ?? 56.56), \(longitude ?? 45.45)")
}
func showCurrentLocation() { <<- NOT BEING CALLED, BUT IF IT IS, CRASHES
//mapView.settings.myLocationButton = true
let locationObj = locationManager.location!
let coord = locationObj.coordinate
let lattitude = coord.latitude
let longitude = coord.longitude
print(" lat in updating \(lattitude) ")
print(" long in updating \(longitude)")
let center = CLLocationCoordinate2D(latitude: locationObj.coordinate.latitude, longitude: locationObj.coordinate.longitude)
let marker = GMSMarker()
marker.position = center
marker.title = "current location"
marker.map = mapView
let camera: GMSCameraPosition = GMSCameraPosition.camera(withLatitude: lattitude, longitude: longitude, zoom: 7.0)
mapView = GMSMapView.map(withFrame: CGRect.zero, camera: camera) <-- CRASHES HERE
self.mapView.animate(to: camera)
}
func fetchUserName(userId: String) {
db.collection("Usuarias").document(self.userID!).getDocument() {
(document, err) in
if let document = document, document.exists {
let dataDescription = document.data()!["Nome"] ?? "nil"
print("Document data: \(dataDescription)")
self.nameUser = dataDescription
print(self.nameUser as Any, "dentro do fetch")
} else {
print("Document does not exist")
}
}
}
}
I'd appreciate any help. Thanks =)
If you've created the 'View' in storyboard and assign as 'GMSMapView' you should add it as 'IBOutlet' in ViewController, you can do it by clicking 'Option' on keyboard and drag from map to ViewController like you've add 'alertaBtn', if you want to create map programmatically you need to initiate it in 'viewDidLoad' method then use it to set camera or to do other changes.

how to get rid of the 'The API method must be called from the main thread' problem when retrieving data from an API to use them in arrays? Swift

I come to you because I have the following problem:
I work with the 'GoogleMaps' cocoapods and I need to place several markers in a map by using the latitude, longitude and a codeID that I get from an API. I will present you guys 2 cases: the one that works (that uses 3 hard coded arrays mentioned before) and the one that I try to get from the API and that crashes no matter what I do. OK, the first case (the one that works) is this one:
import UIKit
import GoogleMaps
class ViewController: UIViewController {
// MARK: - Constants and variables
let lat: Double = 38.739429 // User's Latitude
let lon: Double = -9.137115 // User's Longitude
let zoom: Float = 15.0
// MARK: - Elements in the storyboard
#IBOutlet weak var googleMap: GMSMapView!
// MARK: - ViewDidLoad()
override func viewDidLoad() {
super.viewDidLoad()
googleMapsStuff()
}
// MARK: - Google maps method
func googleMapsStuff() {
googleMap.delegate = self
self.googleMap.isMyLocationEnabled = true // User's current position (blue dot on the map)
let arrayLat: [Double] = [38.739, 38.74, 38.741, 38.732, 38.7325, 38.733]
let arrayLon: [Double] = [-9.136, -9.135, -9.134, -9.137, -9.1375, -9.138]
//var arrayCompanyZoneID: [Int] = []
let camera: GMSCameraPosition = GMSCameraPosition.camera(withLatitude: lat, longitude: lon, zoom: self.zoom)
googleMap.camera = camera
for index in 0..<arrayLon.count {
let marker = GMSMarker()
marker.position = CLLocationCoordinate2D(latitude: arrayLat[index], longitude: arrayLon[index])
marker.title = "Marker number: \(index)"
marker.snippet = "Marker's Lat: \(arrayLat[index]), Marker's Lon: \(arrayLon[index])"
marker.map = self.googleMap
print("Index: \(index)")
}
}
}
And as you can see in the image, it all goes smoothly well
The problem comes in the second case, when I try to fill the empty arrays (which it seems to do) when I connect to an API to get that data. This is the "failure" case:
struct MyInfo: Codable {
let id: String
let name: String
let x: Double // Longitude
let y: Double // Latitude
let licencePlate: String?
let range: Int?
let batteryLevel: Int?
let seats: Int?
let model: String?
let resourceImageId: String?
let pricePerMinuteParking: Int?
let pricePerMinuteDriving: Int?
let realTimeData: Bool?
let engineType: String?
let resourceType: String?
let companyZoneId: Int
let helmets: Int?
let station: Bool?
let availableResources: Int?
let spacesAvailable: Int?
let allowDropoff: Bool?
let bikesAvailable: Int?
}
class ViewController: UIViewController {
// MARK: - Constants and variables
let lat: Double = 38.739429 // User's Latitude
let lon: Double = -9.137115 // User's Longitude
let zoom: Float = 15.0
var arrayLat: [Double] = [] // [38.7395, 38.739, 38.74, 38.741, 38.732, 38.7325, 38.733]
var arrayLon: [Double] = [] // [-9.1365, -9.136, -9.135, -9.134, -9.137, -9.1375, -9.138]
var arrayCompanyZoneID: [Int] = [] // [1, 2, 3, 4, 5, 6, 7]
// MARK: - Elements in the storyboard
#IBOutlet weak var googleMap: GMSMapView!
// MARK: - ViewDidLoad()
override func viewDidLoad() {
super.viewDidLoad()
googleMap.delegate = self
self.googleMap.isMyLocationEnabled = true // User's current position (blue dot on the map)
let camera: GMSCameraPosition = GMSCameraPosition.camera(withLatitude: self.lat, longitude: self.lon, zoom: self.zoom)
googleMap.camera = camera
guard let urlAPI = URL(string: "https://apidev.meep.me/tripplan/api/v1/routers/lisboa/resources?lowerLeftLatLon=38.711046,-9.160096&upperRightLatLon=38.739429,-9.137115") else { return }
let task = URLSession.shared.dataTask(with: urlAPI) {(data, response, error) in
if error == nil {
guard let urlContent = data else { return }
do {
let JSONResult = try JSONDecoder().decode([MyInfo].self, from: urlContent) //JSONSerialization.jsonObject(with: urlContent, options: .mutableContainers)
print("JSON Result:", JSONResult)
for jsonData in JSONResult {
self.arrayLon.append(jsonData.x)
self.arrayLat.append(jsonData.y)
self.arrayCompanyZoneID.append(jsonData.companyZoneId)
}
print("-----------------")
print(type(of: JSONResult))
print("-----------------")
print("ArrayLon:", self.arrayLon)
print("ArrayLat:", self.arrayLat)
print("companyZoneId: ", self.arrayCompanyZoneID)
print("Count zoneid: ", self.arrayCompanyZoneID.count)
print("-----------------")
// MARK: - Place the multiple markers on the map
for index in 0..<self.arrayCompanyZoneID.count {
let marker = GMSMarker()
marker.position = CLLocationCoordinate2D(latitude: self.arrayLat[index], longitude: self.arrayLon[index])
marker.title = "Marker number: \(index)"
marker.snippet = "Marker's Lat: \(self.arrayLat[index]), Marker's Lon: \(self.arrayLon[index])"
marker.map = self.googleMap
print("Index: \(index)")
}
} catch {
print("JSON processing failed.")
}
} else {
print("Error serializing JSON:", error!)
}
}
task.resume()
}
And it doesn't matter what I do, the console always says:
"Terminating app due to uncaught exception 'GMSThreadException', reason: 'The API method must be called from the main thread' "
I also tried using the method
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation])
but it also says that the API method most be called from the main thread.
I'm so stuck here and I invested several hours to this issue but It only fails over and over.
I appreciate your advice and wisdom.
Thanks in advance.
You need
DispatchQueue.main.async {
// MARK: - Place the multiple markers on the map
for index in 0..<self.arrayCompanyZoneID.count {
let marker = GMSMarker()
marker.position = CLLocationCoordinate2D(latitude: self.arrayLat[index], longitude: self.arrayLon[index])
marker.title = "Marker number: \(index)"
marker.snippet = "Marker's Lat: \(self.arrayLat[index]), Marker's Lon: \(self.arrayLon[index])"
marker.map = self.googleMap
print("Index: \(index)")
}
}
As URLSession.shared.dataTask callback is in a background thread

Mapbox Navigation in iOS with in my mapView controller

I want to integrate Mapbox navigation in iOS, I can easily get the direction/route between two coordinate also to get the navigation path from mapbox we can use below code
let options = NavigationOptions(styles: nil)
let viewController = NavigationViewController(for: self.directionsRoute!)
viewController.delegate=self
self.present(viewController, animated: true, completion: nil)
But the problem is I want to display the navigation in my mapview which is a part of another view controller, I can do that by getting a direction/route and instruction but I can't find any method which will be called every second so that I can update route instruction, as well as route, in case of user change the path.
Let me know if I am missing anything or any changes needed.
-Thanks in advance
here is my approach:
first i did get only directions instructions from the MapBox api taking advantage of it's free API calls quota and draw the instructions on GMSMapView or MapKit taking advantage of their good performance and memory management.
podfile
pod 'MapboxDirections.swift'
import MapboxDirections
this is done through the below code
have the property for MapBox directions
#IBOutlet weak var googleMapView: GMSMapView!
let locationManager = CLLocationManager()
let mapBoxirections = Directions(accessToken: osmToken)
var path: GMSMutablePath?
then do the actual api call
private func drawRouteBetween(source: StopModel, destination: StopModel) {
guard let name = source.name, let lat = source.latitude, let lng = source.longitude else { return }
guard let nameDest = destination.name, let latDest = destination.latitude, let lngDest = destination.longitude else { return }
let waypoints = [
Waypoint(coordinate: CLLocationCoordinate2D(latitude: lat, longitude: lng), name: name),
Waypoint(coordinate: CLLocationCoordinate2D(latitude: latDest, longitude: lngDest), name: nameDest),
]
let options = RouteOptions(waypoints: waypoints, profileIdentifier: .automobile)
options.includesSteps = true
options.distanceMeasurementSystem = .metric
mapBoxirections.calculate(options) { (waypoints, routes, error) in
guard error == nil else {
print("Error calculating directions: \(error!)")
return
}
if let route = routes?.first, let leg = route.legs.first {
for step in leg.steps {
if let coordinates = step.coordinates {
for (index, point) in coordinates.enumerated() {
let source = point
if index <= coordinates.count - 2 {
let destination = coordinates[index + 1]
self.drawPolyLine(source: source, destination: destination)
}
}
}
}
}
}
}
note that StopModel is my custom made CLLocation so feel free to replace it with your own as long it has the latitude and longitude
create the method that draws Polyline on your CLLocationManagerDelegate as below
private func drawPolyLine(source: CLLocationCoordinate2D, destination: CLLocationCoordinate2D){
path?.add(source)
path?.add(destination)
let polyLine = GMSPolyline(path: path)
polyLine.strokeWidth = 4 // width of your choice
polyLine.strokeColor = .red // color of your choice
polyLine.map = googleMapView
}
then take a look at the MapBoxDirections.Route model and explore it's properties you will find very useful info inside it
and then take advantage of the callback function from the GMS Delegate that notifies you with the location update instead having a timer and calling it every second this is more efficient way
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
/* do your business here */
}
do not forget to have the delegate of the location manager to self or the class of your choice
Maybe this helps a bit: you can easily add observer for route progress changes:
NotificationCenter.default.addObserver(self,
selector: #selector(progressDidChange(notification:)),
name: .routeControllerProgressDidChange,
object: navigationService.router)
You need a navigation service with your route by creating it like
let navigationService = MapboxNavigationService(route: route)
The function progressDidChange can do something like:
#objc func progressDidChange(notification: NSNotification) {
guard let routeProgress = notification.userInfo?[RouteControllerNotificationUserInfoKey.routeProgressKey] as? RouteProgress,
let location = notification.userInfo?[RouteControllerNotificationUserInfoKey.locationKey] as? CLLocation else {
return
}
// you have all information you probably need in routeProgress, f.E.
let secondsRemaining = routeProgress.currentLegProgress.currentStepProgress.durationRemaining
...
}

Taking latitude & longitude from Cloud Firestore and adding them to Mapbox as annotation

I'm trying to retrieve the flight number, latitude & longitude from all Firestore documents and add them as Mapbox annotations to a mapView. So far the code pulls the data from Cloud Firestore and stores them as variables. Aditionally the code displays a Mapbox map with coordinates but they must be assigned manually in the array
Im having trouble with appending the variables from Firestore to the coordinates array.
I found this "Add annotation after retrieving users latitude and longitude from Firebase" which was along the correct lines but its to do with firebase and not firestore.
Any help would be greatly appreciated!
class HomeViewController: UIViewController, MGLMapViewDelegate {
override func viewDidLoad() {
super.viewDidLoad()
let mapView = MGLMapView(frame: view.bounds)
mapView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
mapView.styleURL = MGLStyle.darkStyleURL
mapView.tintColor = .lightGray
mapView.centerCoordinate = CLLocationCoordinate2D(latitude: 0, longitude: 66)
mapView.zoomLevel = 2
mapView.delegate = self
view.addSubview(mapView)
var aircraftArray = [""]
let db = Firestore.firestore()
let AircraftRef = db.collection("LiveAircraftData").getDocuments { (snapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
} else {
for document in snapshot!.documents {
let FlightNumber = document.documentID
let latitude = document.get("Latitude") as! Double
let longitude = document.get("Longitude") as! Double
print(FlightNumber, latitude, longitude)
var Coordinates = CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
aircraftArray.append(Coordinates)
var pointAnnotations = [MGLPointAnnotation]()
for coordinate in Coordinates {
let point = MGLPointAnnotation()
point.coordinate = coordinate
point.title = "\(coordinate.latitude), \(coordinate.longitude)"
pointAnnotations.append(point)
}
mapView.addAnnotations(pointAnnotations)
}
}
}
}
This is what my database looks like
screenshot of code

Resources