Being a good citizen displaying MKRouteSteps - ios

In lack of a better title...
I'm am writing a custom turn-by-turn implementation in an app used for tracking as well as giving directions using CLLocation and MKMapView.
Getting a route for a given address, displaying it with the returned polyline, getting an ETA and a distance and displaying those all seems fairly trivial and easily implemented. One thing that there isn't a lot of guidance on though, is how to display MKRouteStep's returned in the MKRoute object. All guides online either don't bother mentioning them, or skip over them fairly easily by simply displaying them at once in a table view. Obviously not the most gracious solution, and in my opinion a critical part that's missing.
One solution I did find (can't find the SO answer I saw it in) mentioned creating a CLRegion for each step. So I wen't ahead and created my own implementation of it:
private func drawRouteOnMap() {
viewModel.getRouteForDelivery() { route in
if let route = route {
self.polyline = route.polyline
self.mapView.addOverlay(route.polyline, level: .AboveRoads)
//Add the route steps to the currentRouteSteps array for later retrieval
self.currentRouteSteps = route.steps
//Set the routeStepLabels text property to the first step of the route.
if let step = route.steps.first {
self.routeStepLabel.text = step.instructions
}
//Iterate over the steps in the `MKRoute` object. Create each region to monitor with the identifier set to an Int
var i = 0
for step in route.steps {
let coord = step.polyline.coordinate
let region = CLCircularRegion(center: coord, radius: 20.0, identifier: "\(i)")
self.locationManager.startMonitoringForRegion(region)
i += 1
}
}
}
}
Then I can listen to the func locationManager(manager: CLLocationManager, didEnterRegion region: CLRegion) {} delegate method
private func updateUIForRegion(oldRegion:CLRegion) {
if let regionID = Int(oldRegion.identifier),
steps = currentRouteSteps {
//Fetch the step for the next region
let step = steps[regionID + 1]
routeStepLabel.text = step.instructions
}
}
Then when we exit the view controller
#IBAction func shouldDismissDidPress(sender: AnyObject) {
for (_, region) in locationManager.monitoredRegions.enumerate() {
locationManager.stopMonitoringForRegion(region)
}
dismissViewControllerAnimated(true, completion: nil)
}
This all works fairly well actually. But where things start to fall apart is if a user presses the home button when in the turn by turn navigation VC or if the app crashes. The app will not stop monitoring the regions set up until the locationManager is told to stop monitoring for them, even if the app is force closed. I wouldn't want the app to terminate all the region monitoring anyways, as the navigation wouldn't work when the user resumes the app. It just feels like the region monitoring isn't really the correct way to display the MKRouteStep's, but I'm out of ideas how to do it in another way.
Any ideas / better implementations are welcomed

So, first, the way that works is by definition the most elegant way - so if you have some code that handles every possibility, then by all means go for it!
With that in mind, for cases where there isn't a frank crash but your app is closed could you use one of the many delegate calls in AppDelegate to cease your monitoring?

Related

CLLocationManager not updating while in background, and get messy location coordinates Swift

I have a tracking function but it doesn't update location while in background.
1st case: Tracking while app is in the foreground -> the tracking is actually happening but doesn't get precise coordinates. I will change to locationManager.desiredAccuracy = kCLLocationAccuracyBestForNavigation to see if improves accuracy of the tracking.
2nd case: Tracking while screen is off -> the tracking is a straight line from a to b, tracking doesn't update coordinates.
3rd case: Tracking while app is in back ground(pressed home button) -> tracking is happening as case 1.
I found a post that explains that if authorisation is set to always you have to specify you want to keep updating location while in background, but nothing has changed. This is the code and info.plist :
override func viewDidLoad() {
super.viewDidLoad()
mapView.delegate = self
locationManager.delegate = self
// locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.desiredAccuracy = kCLLocationAccuracyBestForNavigation
// locationManager.allowsBackgroundLocationUpdates = true //for getting user location in background mode as well
mapView.showsUserLocation = true
mapView.userTrackingMode = .follow //map following user
configureLocationServices()
addDoubleTap() // enabling duble tap gesture recognizer
// mapView.isUserInteractionEnabled = true
let location = locationManager.location?.coordinate
let region = MKCoordinateRegionMakeWithDistance(location!, 1000, 1000) // set mapView based on user location coordinates
mapView.setRegion(region, animated: true)
centerMapOnLocation()
// alerts coordinates to post to Firebase
let alertDrawLatitude = alertDrawCoordinates?.latitude // not used ?
let alertDrawLomgitude = alertDrawCoordinates?.longitude
let title: String? = alertNotificationType
var subtitle: String? = alertNotificationType
// user alert notification. takes coordinates from alertNotificationArray( populated with firebase returning coordinate for all alerts
displayAlerts()
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let mostRecentLocation = locations.last else { return }
self.actualRouteInUseCoordinatesArray.append(mostRecentLocation.coordinate)
}
func configureLocationServices() {
if authorizationStatus == .notDetermined{
locationManager.requestAlwaysAuthorization()
} else if authorizationStatus == .authorizedAlways {
locationManager.showsBackgroundLocationIndicator = true //set update location even if in background. very imposrtant!!
}
}
UPDATE:
changing the accuracy only made things worse.
with AccuracyBest:
and with AccuracyBestForNAvigation
second tracking is actually worse.. how can navigation apps rely on this kind of tracking? is there anything wrong with my code for LocationManager?
SECOND UPDATE:
it now get updated location when in background, but is way off..I never passed the yellow street and it shows like I waked for 10 minutes after it..
THIRD EDIT:
I found out that I should filter out GPS raw data, so I'm using a Kalman filter, and it really smooths out the resulting tracking.
So I'm fine tuning two parameters, and in order to be able to change those parameters I added two textfields #IBOutlet weak var filterValueTextField: UITextField! and #IBOutlet weak var horizontalAccuracyTextField: UITextField!and connected those to the parameters
hcKalmanFilter?.rValue = Double(String( describing:filterValueTextField?.text!))! and guard mostRecentLocation.horizontalAccuracy < Double(String( describing: horizontalAccuracyTextField?.text!))! else { return }.
My problem is now that it finds nil while unwrapping value in the horizontalAccuracy parameter.
If in horizontalAccuracy I just put a value it accepts an integer, but when I take it from the texField converting the textfield.text to Int, compiler throws an error Binary operator '<' cannot be applied to operands of type 'CLLocationAccuracy' (aka 'Double') and 'Int', while if I convert it to Double doesn't, but it finds nil.
Why the filterValue finds a value from it's textField, and the horizontal Accuracy doesn't? they're declared, and use the same way.
Any idea?
First of all you have limited time while your app goes to background, and that time is depend upon load on your device's OS, but most probably it is approx. 30 seconds. So this is the reason your are not getting location updates while your screen is off or while your app goes to background.
But Apple allows app to run in background for some tasks and location update is one of them, so you can fetch location updates even if your app goes to background by enabling Background Fetch capability for your app.
For more details please follow below official doc. of Apple:
https://developer.apple.com/documentation/corelocation/getting_the_user_s_location/handling_location_events_in_the_background
And secondly try to maintain your locationmanager object in global scope of your app like you can place it in AppDelegate or in Singleton class if you are maintaining any for your app, so it will always be available.
Sometimes location that you receive does not have desired accuracy, especially when you've just started tracking, first couple of locations are going to be well off. You can use location's horizontal accuracy property to filter location witch have, for example, less then 50m accuracy

Track the time it takes a user to navigate through an iOS app's UI

I want to measure how long (in seconds) it takes users to do certain things in my app. Some examples are logging in, pressing a button on a certain page, etc.
I am using an NSTimer for that. I am starting it in the viewDidLoad of a specific page, and stopping it at the point that I want to measure.
I also want to measure cumulative time for certain things. I would like to start the timer on the log-in screen, and then continue the timer until the user gets to the next view controller and clicks on a certain button.
I'm not sure how to do this. Should create a global variable in my app delegate? Or is there some other better way?
No need for an NSTimer, you just need to record the start times and compare them to the stop times. Try using a little helper class such as:
class MyTimer {
static let shared = MyTimer()
var startTimes = [String : Date]()
func start(withKey key: String) {
startTimes[key] = Date()
}
func measure(key: String) -> TimeInterval? {
if let start = startTimes[key] {
return Date().timeIntervalSince(start)
}
return nil
}
}
To use this, just call start(withKey:) right before you start a long-running task.
MyTimer.shared.start(withKey: "login")
Do something that takes a while and then call measure(key:) when you're done. Because MyTimer is a singleton, it can be called from anywhere in your code.
if let interval = MyTimer.shared.measure("login") {
print("Logging in time: \(interval)")
}
If you're using multiple threads, you may to to add some thread safety to this, but it should work as is in simple scenarios.

Place/City names from a list of CLLocation

Here is my situation. (Using Swift 2.2)
I have a list of coordinates (CLLocation). I need to call the reverseGeocodeLocation to fetch the corresponding Place/City. If I try to loop through the elements there is a chance for some calls to fails as Apple suggest to send one call in a second. So I need to add a delay between each calls as well.
Is there any way to achieve this? Any help is appreciated.
(If we have multiple items with same lat, long we only call the api once)
This code declares a set of locations and looks them up one by one, with at least 1 second between requests:
var locations = Set<CLLocation>()
func reverseGeocodeLocation() {
guard let location = locations.popFirst() else {
geocodingDone()
return
}
CLGeocoder().reverseGeocodeLocation(location) { placemarks, error in
//Do stuff here
//Dispatch the next request in 1 second
_ = Timer.scheduledTimer(withTimeInterval: 1, repeats: false) { _ in
self.reverseGeocodeLocation()
}
}
}
func geocodingDone() {
//Put your finish logic in here
}
FYI I used the block syntax for the Timer, but that only works on iOS 10. If you are using iOS 9 or earlier just use the selector version and it works the same way.

SKMaps and ViaPoints/WayPoints

I have questions regarding skmap sdk functionality.
After creating a route and starting the navigation process, is there anyway of capturing the waypoint/viapoint that you are currently directed towards? After calculating a route, it seems routeInformation.viaPointsOnRoute is empty. Does calculateRoute(route) automatically generate a list of viaPoints? In the SKNavigationDelegate Protocol Reference I see the following event functions :
– routingService:didEnterViaPointArea:
– routingService:didReachViaPointWithIndex:
– routingService:didExitViaPointArea:
However, these methods never seem to be called. Not sure if they are only called if ViaPoints are programmatically added.
Also when the routeService fires the currentAdvice and nextAdvice methods, the SKRouteAvice location appear to be empty as well.
Is there anyway someone could provide me with a simple example of how to create and capture the current waypoint position and the next waypoint position while in navigation? Essentially, I am trying to calculate the direction from the current location to the next waypoint.
Below is rough example of my route and navigation initiation code:
//Created Route
let route: SKRouteSettings = SKRouteSettings()
route.routeMode = SKRouteMode.Pedestrian
route.startCoordinate = coordinateCurrent!
route.destinationCoordinate = coordinateDestination!
route.shouldBeRendered = true
SKRoutingService.sharedInstance().calculateRoute(r oute)
//Start Navigation
let navSettings: SKNavigationSettings = SKNavigationSettings()
navSettings.navigationType = SKNavigationType.Simulation
navSettings.distanceFormat = SKDistanceFormat.MilesFeet
navSettings.viaPointNotificationDistance=1
self.mapView!.settings.displayMode = SKMapDisplayMode.Mode3D
navSettings.transportMode = SKTransportMode.Pedestrian
SKRoutingService.sharedInstance().startNavigationW ithSettings(navSettings)
//Start Postioner Service
SKPositionerService.sharedInstance().delegate = self
SKPositionerService.sharedInstance().startLocation Update()
routeInfo.viaPointsOnRoute
func routingService(routingService: SKRoutingService!, didChangeNextAdvice nextAdvice: SKRouteAdvice, isLastAdvice:Bool)
{
log(String(nextAdvice.location))
}
func routingService(routingService: SKRoutingService!, didChangeCurrentAdvice currentAdvice: SKRouteAdvice, isLastAdvice:Bool)
{
log(String(currentAdvice.location))
}
func routingService(routingService: SKRoutingService!,
didUpdateViaPointsInformation viaPoints: [AnyObject]!)
{
log("VIA INFO: " + String(viaPoints))
}
func routingService(routingService: SKRoutingService!,
didEnterViaPointArea index: Int32)
{
log("ENTER VIA: " + String(index))
}
func routingService(routingService: SKRoutingService!,
didExitViaPointArea index: Int32)
{
log("EXIT VIA: " + String(index))
}
Thank you for your time!
I think that first we have to clarify the terminology.
viaPoint or waypoint (the skobbler/telenav SDK uses the viaPoint terminology) would be a point on the route that is explicitly declared as a "viaPoint". These points are added at route calculation time (by passing an array of viaPoints in the route settings) or during navigation with the addViaPoint API.
These viaPoints will be monitored by the SDK and you'll receive the didEnterViaPointArea, didReachViaPointWithIndex and didExitViaPointArea callbacks.
I believe that you're using the term waypoint by describing a "junction" or "point requiring an advice" along the route - these would mostly correspond to the "route coordinates" provided by the SDK (see the routeCoordinationsForRouteWithID API). These route coordinates are not explicitly surfaced by the SDK - you don't get any explicit information about these points but only indirect notifications in the form of advices ("In 200 meters turn right onto Main Street").
What the turn by turn engine provides you is exactly the "direction" between 2 such coordinates (in the form of an advice: "carry straight on", "turn right", "turn left"). See the documentation regarding how the advices are created for more details (here and here)
Maybe the skobbler/Telenav team can provide you better advice if you could describe your end goal (what use case you are trying to achieve).

Best Reactive-Cocoa approach for writing a CLLocationManagerDelegate, which will infrequently fetch location

Background
I'm really excited by the ReactiveCocoa framework and the potential it has, so I've decided that I'm going to bite the bullet and write my first app using it.
In my app, I've already written the various services and delegates, but I now need to 'Reactive-Cocoa-ise' them so that I can get on with actual GUI side of things.
That said, so that I better understand this, I'm writing a simple bit of code just to try out concepts.
In this case, writing a wrapper for CLLocationManagerDelegate.
In the actual app, the use case would be this:
1) When the app is loaded up (viewDidLoad) then 2) Attempt to fetch
the location of the device by
2.1) if location services not enabled then
2.1.1) check authorisation status, and if allowed to startMonitoringSignificantLocationChanges,
2.1.2) else return an error
2.2) else (location services are enabled)
2.2.1) if the location manager last location was 'recent' (6 hours or less) return that
2.2.2) else startMonitoringSignificantLocationChanges
3) when returning the location (either straight away, or via
locationManager:(CLLocationManager *)manager
didUpdateLocations:(NSArray *)locations), then we also stopMonitoringSignificantLocationChanges
4) If code that calls on the LocationManagerDelegate receives and error, then ask the delegate for a fixed 'default' value
5) Next piece of code then uses the location (either fetched, or default) to go off and do a bunch of calls on third party services (calls to WebServices etc)
You can see a test harness at https://github.com/ippoippo/reactive-core-location-test
Concerns
The harness 'works' in that it goes off and fetches the location. However, I'm really concerned I'm doing this in the right way.
Consider this code
RACSignal *fetchLocationSignal = [lmDelegate latestLocationSignal];
RACSignal *locationSignal = [fetchLocationSignal catch:^RACSignal *(NSError *error) {
NSLog(#"Unable to fetch location, going to grab default instead: %#", error);
CLLocation *defaultLocation = [lmDelegate defaultLocation];
NSLog(#"defaultLocation = [%#]", defaultLocation);
// TODO OK, so now what. I just want to handle the error and
// pass back this default location as if nothing went wrong???
return [RACSignal empty];
}];
NSLog(#"Created signal, about to bind on self.locationLabel text");
RAC(self.locationLabel, text) = [[locationSignal filter:^BOOL(CLLocation *newLocation) {
NSLog(#"Doing filter first, newLocation = [%#]", newLocation);
return newLocation != nil;
}] map:^(CLLocation *newLocation) {
NSLog(#"Going to return the coordinates in map function");
return [NSString stringWithFormat:#"%d, %d", newLocation.coordinate.longitude, newLocation.coordinate.latitude];
}];
Questions
1) How do I handle errors? I can use catch which then gives me the opportunity to then ask the delegate for a default location. But, I'm then stuck on how to then pass back that default location as a signal? Or do I just change my defaultLocation method to return a RACSignal rather than CLLocation??
2) When I return the location, should it be done as sendNext or sendCompleted? Currently it's coded as sendNext, but it seems like something that would be done as sendCompleted.
3) Actually, does the answer to that depend on how I create the Delegate. This test app creates a new Delegate each time the view is loaded. Is that something I should do, I should I make the Delegate a singleton. If it's a singleton, then sendNext seems the right thing to do. But if would then imply that I move the code that I have in latestLocationSignal in my delegate into an init method instead?
Apologies for the rambling question(s), just seem to be going around in circles in my head.
Updating the answer above to ReactiveCocoa 4 and Swift 2.1:
import Foundation
import ReactiveCocoa
class SignalCollector: NSObject, CLLocationManagerDelegate {
let (signal,sink) = Signal<CLLocation, NoError>.pipe()
let locationManager = CLLocationManager()
func start(){
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.startUpdatingLocation()
}
func locationManager(manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
for item in locations {
guard let location = item as CLLocation! else { return }
sink.sendNext(location)
}
}
And to listen the events:
var locationSignals : [CLLocation] = []
signal.observeNext({ newLocation in
locationSignals.append(newLocation)
})
How do I handle errors? I can use catch which then gives me the opportunity to then ask the delegate for a default location.
You're so close. Use +[RACSignal return:] to create a signal which just sends your default location:
RACSignal *locationSignal = [fetchLocationSignal catch:^RACSignal *(NSError *error) {
NSLog(#"Unable to fetch location, going to grab default instead: %#", error);
CLLocation *defaultLocation = [lmDelegate defaultLocation];
NSLog(#"defaultLocation = [%#]", defaultLocation);
return [RACSignal return:defaultLocation];
}];
When I return the location, should it be done as sendNext or sendCompleted? Currently it's coded as sendNext, but it seems like something that would be done as sendCompleted.
In order to actually send data, you'll need to use -sendNext:. If that's the only thing that the signal is sending, then it should also -sendCompleted after that. But if it's going to continue to send the location as accuracy improves or location changes, then it shouldn't complete yet.
More generally, signals can send any numbers of nexts (0*) but can only complete or error. Once completion or error is sent, the signal's done. It won't send any more values.
Actually, does the answer to that depend on how I create the Delegate. This test app creates a new Delegate each time the view is loaded. Is that something I should do, I should I make the Delegate a singleton. If it's a singleton, then sendNext seems the right thing to do. But if would then imply that I move the code that I have in latestLocationSignal in my delegate into an init method instead?
I'm not sure I have enough context to answer this, except to say that singletons are never the answer ;) Creating a new delegate each time the view is loaded seems reasonable to me.
It's hard to answer broad questions like this because I don't know your design nearly as well as you do. Hopefully these answers help a bit. If you need more clarification, specific examples are really helpful.
Seems to be super easy in ReactiveCocoa 3.0 and swift :
import ReactiveCocoa
import LlamaKit
class SignalCollector: NSObject, CLLocationManagerDelegate {
let (signal,sink) = Signal<CLLocation, NoError>.pipe()
let locationManager = CLLocationManager()
func start(){
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.startUpdatingLocation()
}
func locationManager(manager: CLLocationManager!, didUpdateLocations locations: [AnyObject]!) {
for item in locations {
if let location = item as? CLLocation {
sink.put(Event.Next(Box(location))
}
}
}
}
And wherever you need to listen to the location events, use the signal with an observer :
var locationSignals : [CLLocation] = []
signal.observe(next: { value in locationSignals.append(value)})
Note that since it's pre-alpha at the moment the syntax is likely to change.

Resources