how to match (or compare) taps to annotations? - ios

EnvironmentXcode 8Swift 3
Problem Statement
I want to be able to determine if a user taps on a MKPointAnnotation and then extract information (like title and subtitle) from that annotation for use within my app.
I imagine this is not terribly difficult, but I'm a bit lost in terms of what I need to do / what various classes / objects / methods / etc. I need to use to do this.So I'm looking for pointers / guidance - code is welcome, but at this point the pointers / guidance would be a significant step forward for me.
Code SnippetsAbridged version of the code thus far (trying to limit it to just the relevant pieces)
class NewLocationViewController: UIViewController, CLLocationManagerDelegate, UITextFieldDelegate {
//... various #IBOutlet's for text fields, buttons, etc. ...
#IBOutlet weak var map: MKMapView!
var coords: CLLocationCoordinate2D?
var locationManager: CLLocationManager = CLLocationManager()
var myLocation: CLLocation!
var annotation: MKPointAnnotation!
var annotationList: [MKPointAnnotation] = []
var matchingItems: [MKMapItem] = [MKMapItem]()
override func viewDidLoad() {
super.viewDidLoad()
//... text field delegates, and other initilizations ...
locationManager.requestWhenInUseAuthorization()
if CLLocationManager.locationServicesEnabled() {
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.delegate = self
}
myLocation = nil
//... other initializations...
}
// Search for things that match what my app is looking for ("<search string>")
func performSearch() {
annotationList.removeAll() // clear list
matchingItems.removeAll() // clear list
var closest = MKMapItem()
var distance = 10000.0
let request = MKLocalSearchRequest()
let span = MKCoordinateSpan(latitudeDelta: 0.001, longitudeDelta: 0.001)
request.naturalLanguageQuery = "<search string>"
request.region = MKCoordinateRegionMake(myLocation.coordinate, span)
let search = MKLocalSearch(request: request)
if search.isSearching {
search.cancel()
}
search.start(completionHandler: {
(_ response, _ error) in
if error != nil {
self.showAlert(msg: "Error occurred in search\nERROR: \(error?.localizedDescription)")
}
else if response!.mapItems.count == 0 {
self.showAlert(msg: "No matches found")
}
else {
for item in response!.mapItems {
// Track the closest placemark to our current [specified] location
let (distanceBetween, prettyDistance) = self.getDistance(loc1: self.myLocation, loc2: item.placemark.location!)
let addrObj = self.getAddress(placemark: item.placemark)
//... some code omitted ...
// Add markers for all the matches found
self.matchingItems.append(item as MKMapItem)
let annotation = MKPointAnnotation()
annotation.coordinate = item.placemark.coordinate
annotation.title = item.name
annotation.subtitle = "\(addrObj.address!) (\(prettyDistance))"
self.map.addAnnotation(annotation)
self.annotationList.append(annotation)
}
//... some code omitted ...
}
})
}
//... code for getDistance(), getAddress() omitted for brevity - they work as designed ...
//... other code omitted as not being relevant to the topic at hand
}
I imagine that I will need to override touchesEnded and possibly touchesBegan and maybe touchesMoved in order to detect the tap.
What I cannot figure out is how to compare a touch's location (represented as X/Y coordinates on the screen) to an MKPointAnnotation's or MKMapItem's location (which is represented as latitude/longitude coordinates on a map)
So - that's kind of where I'm currently stuck. I searched various terms on the web but wasn't able to find anything that [simplisticly] answerwed my question - and in Swift code format (there were a number of postings that looked like they might help, but the code presented wasn't in Swift and I don't do the translation that easily).
UPDATE (19:48 ET)
I found this article: How do I implement the UITapGestureRecognizer into my application and tried to follow it, but ...
I modified the code a bit (added UIGestureRecognizerDelegate):
class NewLocationViewController: UIViewController, CLLocationManagerDelegate, UITextFieldDelegate, UIGestureRecognizerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
//...other code...
let tapHandler = UITapGestureRecognizer(target: self, action: Selector(("handleTap:"))) //<<<== See notes below
tapHandler.numberOfTapsRequired = 1
tapHandler.numberOfTouchesRequired = 1
tapHandler.delegate = self
print("A")//#=#
map.addGestureRecognizer(tapHandler)
map.isUserInteractionEnabled = true
print("B")//#=#
}
func handleTap(tap: UITapGestureRecognizer) {
print("ARRIVED")//#=#
let here = tap.location(in: map)
print("I AM HERE: \(here)")//#=#
}
//...
}
With regard to the declaration / definition of tapHandler, I tried the following:
let tapHandler = UITapGestureRecognizer(target: self, action: "handleTap:")
let tapHandler = UITapGestureRecognizer(target: self, action: Selector("handleTap:"))
let tapHandler = UITapGestureRecognizer(target: self, action: Selector(("handleTap:"))) // supresses warning
The first two caused a warning to show up in Xcode, the last simply supresses the warning:
[W] No method declared with Objective-C selector 'handleTap:'
When I run my app and tap on a pin - I get the following in my log:
A
B
libc++abi.dylib: terminating with uncaught exception of type NSException
Which would seem (to me) to indicate that the general setup in viewDidLoad is okay, but as soon as it tries to handle the tap, it dies without ever getting to my handleTap function - and thus the warning (shown above) would seem to be far more serious.
So, I'm not sure if I can count this as making progress, but I'm trying...

Thanks to this MKAnnotationView and tap detection I was able to find a solution. My code changes from those originally posted:
class NewLocationViewController: UIViewController, CLLocationManagerDelegate, UITextFieldDelegate, UIGestureRecognizerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
//...other code...
let tapHandler = UITapGestureRecognizer() //<<<== No parameters
tapHandler.numberOfTapsRequired = 1
tapHandler.numberOfTouchesRequired = 1
tapHandler.delegate = self
map.addGestureRecognizer(tapHandler)
map.isUserInteractionEnabled = true
}
// Not sure who calls this and requires the Bool response, but it seems to work...
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
return self.handleTap(touch: touch).count > 0
}
// Major Changes
private func handleTap(touch: UITouch) -> [MKAnnotationView] {
var tappedAnnotations: [MKAnnotationView] = []
for annotation in self.map.annotations {
if let annotationView: MKAnnotationView = self.map.view(for: annotation) {
let annotationPoint = touch.location(in: annotationView)
if annotationView.bounds.contains(annotationPoint) {
self.name.text = annotationView.annotation?.title!
let addr = AddrInfo(composite: ((annotationView.annotation?.subtitle)!)!)
self.address.text = addr.street!
self.city.text = addr.city!
self.state.text = addr.state!
self.zipcode.text = addr.zip!
tappedAnnotations.append(annotationView)
break
}
}
}
return tappedAnnotations
}
//...
}
The AddrInfo piece is my own little subclass that, among other things, takes a string like "1000 Main St., Pittsburgh, PA 15212, United States" and breaks it into the individual pieces so that they can be accessed, well, individually (as indicated in the code above).
There might be an easier, or better, way to achieve what I was looking for - but the above does achieve it, and so I consider it to be the answer for my issue.

Related

How to identify if MKPointAnnotation has been pressed in Swift?

Using a for loop I have stored a unique URL in each annotation using a tag create in a class named CustomPointAnnotation. I am trying to print out the URL of the annotation that has been pressed. The problem is my output console in Xcode prints nothing when I click on an annotation.
I tried to follow this guide: How to identify when an annotation is pressed which one it is
I replicated all the code but it is not detecting if the annotation was clicked.
How do I know if the annotation is clicked?
Here is the CustomPointAnnotation.
class CustomPointAnnotation: MKPointAnnotation {
var tag: String!
}
I declared the variable tag so I can store a unique variable for each annoation.
My ViewController class:
In the ViewController class there is a loop which iterates through my Firebase Database JSON files:
func displayCordinates() {
ref = Database.database().reference()
let storageRef = ref.child("waterfountains")
storageRef.observeSingleEvent(of: .value, with: { snapshot in
for child in snapshot.children.allObjects as! [DataSnapshot] {
let annotation = CustomPointAnnotation()
let dict = child.value as? [String : AnyObject] ?? [:]
annotation.title = "Water Fountain"
annotation.tag = dict["url"] as? String
annotation.coordinate = CLLocationCoordinate2D(latitude: dict["lat"] as! Double, longitude: dict["long"] as! Double)
self.mapView.addAnnotation(annotation)
}
})
}
The annotations are displayed by calling the functions in viewDidLoad:
override func viewDidLoad() {
super.viewDidLoad()
displayCordinates()
}
A function which should detect if an annotation has been clicked:
func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) {
if let annotation = view.annotation as? CustomPointAnnotation {
print(annotation.tag!)
}
}
Thank you for helping.
mapView:didSelect: is a MKMapViewDelegate method. If you do not set mapView.delegate = self on your ViewController this function will never get triggered.
Typically it would get set in ViewDidLoad. before performing any other operations with the mapView. Changing your ViewDidLoad to look like
override func viewDidLoad() {
super.viewDidLoad()
self.mapView.delegate = self
displayCordinates()
}
should fix your issue. For more information on the protocol/delegate design pattern all over the apple frameworks, I suggest this swift article from the Swift Programming Guide.
More specifically to your case, check out all the other functionality/control you can bring with your implementation of MKMapView on your ViewController by checkout out the apple docs on MKMapViewDelegate. This will cover things like monitoring when the map finishes loading, when it fails, when the user's location is updated, and more things you may want to increase the functionality of your app and provide a great user experience.

MKMapView is nil, but the IBOutlet is correct initialized

I am making my first XCode project and I want to make an app where you can click on a tableviewcell and then you can see are directed to the next detailpage where the location is on the map. The problem is that I get a thread when i run the app:
Fatal error: Unexpectedly found nil while unwrapping an Optional value
and in my output i see that (MKMapView!) nil is.
This is my code on my MapViewController (the detailpage where the map is supposed to be)
import UIKit
import MapKit
class MapViewController: UIViewController, MKMapViewDelegate{
#IBOutlet weak var mijnMap: MKMapView!
var currentDetail: Parking? {
didSet {
showInfoDetail()
}
}
override func viewDidLoad() {
super.viewDidLoad()
}
func showInfoDetail() {
if let detail = currentDetail {
let coordinaat = CLLocationCoordinate2D(latitude: CLLocationDegrees(detail.latitude)
, longitude: CLLocationDegrees(detail.longitude))
MKMapPointForCoordinate(coordinaat)
mijnMap.centerCoordinate = coordinaat
mijnMap.region = MKCoordinateRegionMakeWithDistance(coordinaat, 1000, 1000)
let parkingAnnotation = MKPointAnnotation()
parkingAnnotation.coordinate = coordinaat
parkingAnnotation.title = detail.title
parkingAnnotation.subtitle = detail.city
mijnMap.addAnnotation(parkingAnnotation)
}
}
}
I get the threat on this line mijnMap.centerCoordinate = coordinaat
Can someone help me with this? Thanks!
The problem is that because of didSet in
var currentDetail: Parking? {
didSet {
showInfoDetail()
}
}
when you try to set currentDetail from another VC the call to method showInfoDetail triggers and this happen before the VC is presented which means prior to view loading resulting in a crash as all IBOutlets are not yet been initialized , so delay the call to it in viewDidAppear or check the map before usage
var currentDetail: Parking? {
didSet {
if mijnMap != nil {
showInfoDetail()
}
}
}
Note in second option you have at call showInfoDetail in viewDidLoad / viewDidAppear , I'm not suggesting removing didSet as you may want to change that string value in MapViewController and trigger the same function every change

Set route on a map using Indoo.rs ios sdk

I am using Indoo.rs ios sdk by swift to build a small app showing my map, the issue is I have no idea to write a code to set route between 2 point on the map, I came across the documentation but no result ... here is my code :
The documentation is here : https://indoors.readme.io/docs/routing-1
Please if there is any idea share with me.
Thanks
import UIKit
import CoreLocation
class ViewController: UIViewController , RoutingDelegate ,IndoorsServiceDelegate, ISIndoorsSurfaceViewControllerDelegate ,IndoorsSurfaceViewDelegate {
#IBOutlet var SetRouteButton: UIButton!
var _currentBuilding: IDSBuilding?
var dot : ISIndoorsSurface?
var _indoorsSurfaceViewController: ISIndoorsSurfaceViewController?
override func viewDidLoad() {
super.viewDidLoad()
// API key of the cloud application
_ = Indoors(licenseKey: "My_API_KEY" , andServiceDelegate: nil)
_indoorsSurfaceViewController = ISIndoorsSurfaceViewController()
_indoorsSurfaceViewController!.delegate = self
// Enabling dotOnRail Mode
_indoorsSurfaceViewController!.surfaceView.dotOnRailsJumpingDistance = 55000
_indoorsSurfaceViewController!.surfaceView.enableDotOnRails = true
// show currunt postion
_indoorsSurfaceViewController!.surfaceView.showsUserPosition = true
// Load the map in a view holding it
addSurfaceAsChildViewController()
_indoorsSurfaceViewController!.loadBuildingWithBuildingId(MyBuildingId)
// Route snaping
Indoors.instance().enablePredefinedRouteSnapping()
// Display All Zones
_indoorsSurfaceViewController!.surfaceView.setZoneDisplayMode(IndoorsSurfaceZoneDisplayModeAllAvailable)
// Set visible map
let mapRect: CGRect = _indoorsSurfaceViewController!.surfaceView.visibleMapRect
_indoorsSurfaceViewController!.surfaceView.setVisibleMapRect(mapRect, animated: true)
// Filters
Indoors.instance().enableStabilisationFilter = true
Indoors.instance().stabilisationFilterTime = 4000
}
// Add the map to the view controller
func addSurfaceAsChildViewController () {
self.addChildViewController(_indoorsSurfaceViewController!)
//_indoorsSurfaceViewController!.view.frame = self.view.frame
_indoorsSurfaceViewController!.view.frame = CGRectMake(0 , 0, self.view.frame.width, self.view.frame.height * 0.7)
self.view.addSubview(_indoorsSurfaceViewController!.view)
_indoorsSurfaceViewController!.didMoveToParentViewController(self)
}
func buildingLoaded(building : IDSBuilding!) {
_currentBuilding = building
self.calculateRoute(_currentBuilding)
print("##################")
}
func calculateRoute(building : IDSBuilding!) {
let start = IDSCoordinate(x: 1, andY: 111, andFloorLevel: 0);
let end = IDSCoordinate(x: 1, andY: 111, andFloorLevel: 0);
let path = [start, end]
Indoors.instance().routeFromLocation(start, toLocation: end, inBuilding: building, delegate: self)
self.setRoute(path)
}
// MARK: RoutingDelegate
func setRoute(path: [AnyObject]!) {
_indoorsSurfaceViewController!.surfaceView.showPathWithPoints(path)
}
}
extension ViewController {
// MARK: ISIndoorsSurfaceViewControllerDelegate
func indoorsSurfaceViewController(indoorsSurfaceViewController: ISIndoorsSurfaceViewController!, isLoadingBuildingWithBuildingId buildingId: UInt, progress: UInt) {
NSLog("Building loading progress: %lu", progress)
}
func indoorsSurfaceViewController(indoorsSurfaceViewController: ISIndoorsSurfaceViewController!, didFinishLoadingBuilding building: IDSBuilding!) {
NSLog("Building loaded successfully!")
// By Mohammed Hassan
UIAlertView(title: "Indoors", message: "Building loaded successfully!", delegate: nil, cancelButtonTitle: nil, otherButtonTitles: "ok").show()
}
func indoorsSurfaceViewController(indoorsSurfaceViewController: ISIndoorsSurfaceViewController!, didFailLoadingBuildingWithBuildingId buildingId: UInt, error: NSError!) {
NSLog("Loading building failed with error: %#", error)
UIAlertView(title: error!.localizedDescription, message: error!.localizedFailureReason!, delegate: nil, cancelButtonTitle: nil, otherButtonTitles: "ok").show()
}
// MARK: IndoorsServiceDelegate
func onError(indoorsError: IndoorsError!) {
}
func locationAuthorizationStatusDidChange(status: IDSLocationAuthorizationStatus) {
}
func bluetoothStateDidChange(bluetoothState: IDSBluetoothState) {
}
}
The documentation you linked shows that the points are not simple arrays of integers (that would be weird, imo). You're casting the array point to an array of AnyObjects (still not points, and since you used integers it won't work anyways) and pass that. You need to construct an array of IDSCoordinates (for start and end) to use as path. Like the documentation suggests (converted to swift):
var start = IDSCoordinate(x: 1234, andY: 1234, andFloorLevel: 0)
var end = IDSCoordinate(x: 12345, andY: 12345, andFloorLevel: 0)
// 1234 and so on are example values, that depends on your context
Indoors.instance().routeFromLocation(start, toLocation: end, delegate: self)
I haven't used the SDK (though I know the guys from indoo.rs, nice fellas), so I don't know when that delegate method setRoute is called exactly, the documentation example seems to update a view to show the path.
Edit: I deleted the setRoute call in my example, because on second thought I began to wonder why you were calling this delegate method in the first place. It's a bad name for a delegate method, true (more like a setter), but if I understand the documentation correctly, this is called for you, hence it's a delegate method. You're supposed to call Indoors.instance().routeFromLocation(start, toLocation: end, delegate: self). I assume, that then at some point calls your delegate's setRoute.

Delegate: MKMapView is nil, though it is initialized

When I receive in my Bluetooth class (new) values from my device, then I call a delegate. So the Bluetooth class is running in the background.
My protocol is simple:
protocol RefreshPositionInDrive {
func changingValue(latitude: Double, longitude: Double)
}
In my UIViewController I initialize a map. When I worked at the beginning without delegates this code works fine.
func initializeMapResolution() {
let regionRadius: CLLocationDistance = 1000
let initialLocation = CLLocation(latitude: 50.910349, longitude: 8.066895)
let coordinateRegion = MKCoordinateRegionMakeWithDistance(initialLocation.coordinate,
regionRadius * 1.5, regionRadius * 1.5)
MapDrive.setRegion(coordinateRegion, animated: true)
MapDrive.delegate = self
}
My method from my protocol:
func changingValue(latitude: Double,longitude: Double) {
print("THE NEW COORDINATES \(latitude) \(longitude)")
if self.MapDrive == nil {
print("Is nil")
} else {
updateTheMap()
}
}
Output:
THE NEW COORDINATES 25.012x 16.992
Is nil
But I don't understand that. I initialize my map first. After that the changingValue is called. How can be the MapDrive nil?
I tested the code without delegates, just with some fix coordinates in my UIViewController and the annotation appears.
(I'm working the first time with delegates.)
EDIT
I was indistinctly: My MapDrive:
#IBOutlet weak var MapDrive: MKMapView!
So I can't instantiate like you mean or?
You'll want to reference a MapDrive instance to the UIViewController, your MapDrive is probably released when your function ends.
class UIViewController {
var mapDrive = MapDrive()
func initializeMapResolution() {
// Instantiate map drive, assign it to self.mapDrive
//then assign the delegate of the property on self.
self.mapDrive.delegate = self
}
}

Displaying geopoint of all users in a map parse

I'am trying to query an array of PFGeoPoints stored on the Parse backend. I have the User table, with data assigned to it such as "location", "name".
everything is being sent to Parse upon posting from my app and is properly stored in the backend. I am having issues retrieving all location from Parse and storing them into an MKAnnotation on the map.
Find below my code
import UIKit
import Parse
import CoreLocation
import MapKit
class mapViewController: UIViewController, MKMapViewDelegate, CLLocationManagerDelegate {
#IBOutlet var mapUsers: MKMapView!
var MapViewLocationManager:CLLocationManager! = CLLocationManager()
var currentLoc: PFGeoPoint! = PFGeoPoint()
override func viewDidLoad() {
super.viewDidLoad()
// ask user for their position in the map
PFGeoPoint.geoPointForCurrentLocationInBackground {
(geoPoint: PFGeoPoint?, error: NSError?) -> Void in
if let geoPoint = geoPoint {
PFUser.currentUser()? ["location"] = geoPoint
PFUser.currentUser()?.save()
}
}
mapUsers.showsUserLocation = true
mapUsers.delegate = self
MapViewLocationManager.delegate = self
MapViewLocationManager.startUpdatingLocation()
mapUsers.setUserTrackingMode(MKUserTrackingMode.Follow, animated: false)
}
override func viewDidAppear(animated: Bool) {
let annotationQuery = PFQuery(className: "User")
currentLoc = PFGeoPoint(location: MapViewLocationManager.location)
annotationQuery.whereKey("Location", nearGeoPoint: currentLoc, withinMiles: 10)
annotationQuery.findObjectsInBackgroundWithBlock {
(PFUser, error) -> Void in
if error == nil {
// The find succeeded.
print("Successful query for annotations")
let myUsers = PFUser as! [PFObject]
for users in myUsers {
let point = users["Location"] as! PFGeoPoint
let annotation = MKPointAnnotation()
annotation.coordinate = CLLocationCoordinate2DMake(point.latitude, point.longitude)
self.mapUsers.addAnnotation(annotation)
}
} else {
// Log details of the failure
print("Error: \(error)")
}
}
}
Instead of putting the call in your viewDidAppear() method - as previous commenters said, your location may be nil, returning no results - I would use a tracker of some sort and put it in your didUpdateLocations() MKMapView delegate method. Here, I use GCD's dispatch_once() so that when my location is found for the first time with a reasonable accuracy, my code is executed (here you will put your call to Parse).
Declare GCD's tracker variable
var foundLocation: dispatch_once_t = 0
Now use something like this in your location manager's delegate method didUpdateLocations()
if userLocation?.location?.horizontalAccuracy < 2001 {
dispatch_once(&foundLocation, {
// Put your call to Parse here
})
}
PS. You should also consider doing any updates to UI on the main thread. Parse fetches that data on a background thread and although you might never see a problem, it's both safer and good habit to do any UI changes on main thread. You can do this with the following:
dispatch_async(dispatch_get_main_queue(), {
// Put any UI update code here. For example your pin adding
}

Resources