I am rather new to Objective-C development so please be patient!
I am trying to take data from a user & send that information to an external database. I have managed to work out how to push the data rather easily, but the issue is; due to the nature of my application, there is a high probability that the user will have no mobile connectivity while they're using it. How can I continuously check that the user has mobile connectivity, and then send the data when it's connected? My code for the action is below:
(Just to clarify, the action takes 10 readings of signal over 5 seconds, appends them to an array, calculates the average and the updates the location. In turn, the locationManager sends the data to a cloud service including the average signal reading.
- (IBAction)buttonPressed:(id)sender {
// Make Manager Delegate & Set Accuracy
manager.delegate = self;
manager.desiredAccuracy = kCLLocationAccuracyBest;
// Call Timer
myTimer = [NSTimer scheduledTimerWithTimeInterval:0.5
target:self
selector:#selector(arrayBuild)
userInfo:nil
repeats:YES];
// Initialise Array
resultsArray = [[NSMutableArray alloc]init];
}
#pragma mark CLLocationManagerDelegate Methods
- (void)locationManager:(CLLocationManager *)manager didFailWithError:(NSError *)error {
NSLog(#"Error: %#", error);
NSLog(#"Failed to get location!");
}
-(void)locationManager:(CLLocationManager *)manager didUpdateToLocation:(CLLocation *)newLocation fromLocation:(CLLocation *)oldLocation {
NSLog(#"Location: %#",newLocation);
if (curentLocation != nil) {
// Code below uses a third party utility to submit data
PFObject *Object = [PFObject objectWithClassName:#"Issue"];
Object[#"Latitude"] = [NSString stringWithFormat:#"%.8f",curentLocation.coordinate.latitude];
Object[#"Longitude"] = [NSString stringWithFormat:#"%.8f",curentLocation.coordinate.longitude];
Object[#"Signal"] = [NSString stringWithFormat:#"%#",_avgNumber];
[Object saveInBackground];
// Update Text Fields
self.latitude.text = [NSString stringWithFormat:#"%.8f",curentLocation.coordinate.latitude];
self.longitude.text = [NSString stringWithFormat:#"%.8f",curentLocation.coordinate.longitude];
self.signal.text = [NSString stringWithFormat:#"%#",_avgNumber];
}
// Stop the Location Update
[manager stopUpdatingLocation];
}
- (void)arrayBuild {
loopCount++;
if (loopCount >= 11) {
// Find Average
NSNumber *avg = [resultsArray valueForKeyPath:#"#avg.self"];
_avgNumber = avg;
// Change Text of Label to Average & Log
self.signal.text = [NSString stringWithFormat:#"%#",_avgNumber];
NSLog(#"%#",_avgNumber);
// Update Location
[manager startUpdatingLocation];
// Invalidate Timer
[myTimer invalidate];
myTimer = nil;
}else{
// Declare Signal Strength
float signalstrength = CTGetSignalStrength();
// Individual Result & Convert to Integer
NSString *result = [NSString stringWithFormat:#"%f", signalstrength];
NSInteger resultInt = [result integerValue];
// Add Object
[resultsArray addObject:[NSNumber numberWithFloat:resultInt]];
// Change Text of Label Each Second
self.signal.text = [NSString stringWithFormat:#"%d",loopCount];
NSLog(#"%f",signalstrength);
}
}
What you need is called "Network Reachability Monitoring". Once you subscribe to it, your will be notified when there are changes to network connectivity state, i.e. device became online or offline, and even type of current connection (WLAN or WWAN).
There is a sample project from Apple and third-party networking libraries (such as AFNetworking) often provide a convenience class for better experience.
EDIT:: Easier solution is to is to use Parse SDK and their saveEventually method instead of saveInBackground. This will, according to documentation, take care of situations when network is not accessible.
There are many question on forum addressing this issue. Apple provides Reachability class for this purpose. You may check This or this question for further clarification.
Related
I have researched on how to get location data from images returned from UIImagePickerController camera. However, I think that the easiest way is to get the current location from CLLocationManager at the instant UIImagePickerController captures an image.
Is there a way of doing this? Is there a way of listening for the "capturePhoto" event, for example?
Just to clarify, the users using my app will likely be moving pretty fast.
Here's what I'd recommend so you don't track the user's location any more than you have to and so you get the user's location closest to the time the image was actually snapped.
Instantiate the CLLocationManager class variable in your viewDidLoad, ex:
self.locationManager = [[CLLocationManager alloc] init];
self.locationManager.delegate = self;
And make sure it's authorized:
if ([CLLocationManager authorizationStatus] != kCLAuthorizationStatusAuthorizedWhenInUse) {
[self.locationManager requestWhenInUseAuthorization];
}
(Also include the "NSLocationWhenInUseUsageDescription" key in the .plist)
Then you could wait until the UIImagePickerController is actually presented before (1) initializing the dictionary to hold the locations and (2) starting to update the location, ex:
[self presentViewController:self.imagePicker animated:YES completion:nil];
self.locationDictionary = [[NSMutableDictionary alloc] init];
[self.locationManager startUpdatingLocation];
At that point, you can start storing the user's updated locations in an NSMutableDictionary self.locationDictionary class instance variable when CLLocation values are returned from the didUpdateToLocation delegate method, ex:
- (void)locationManager:(CLLocationManager *)manager didUpdateToLocation:(CLLocation *)newLocation fromLocation:(CLLocation *)oldLocation {
// Format the current date time to match the format of
// the photo's metadata timestamp string
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
[formatter setDateFormat:#"YYYY:MM:dd HH:mm:ss"];
NSString *stringFromDate = [formatter stringFromDate:[NSDate date]];
// Add the location as a value in the location NSMutableDictionary
// while using the formatted current datetime as its key
[self.locationDictionary setValue:newLocation forKey:stringFromDate];
}
And then once the image is selected, find its timestamp in the metadata and find the value in the location dictionary with a timestamp key closest to the image timestamp, ex:
- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info {
[self.locationManager stopUpdatingLocation];
// When a photo is selected save it as a UIImage
self.selectedPhoto = info[UIImagePickerControllerOriginalImage];
// Get the timestamp from the metadata and store it as an NSString
self.selectedPhotoDateTime = [[[info valueForKey:UIImagePickerControllerMediaMetadata] objectForKey:#"{Exif}"] objectForKey:#"DateTimeOriginal"];
// If the CLLocationManager is in fact authorized
// and locations have been found...
if (self.locationDictionary.allKeys.count > 0) {
// Sort the location dictionary timestamps in ascending order
NSArray *sortedKeys = [[self.locationDictionary allKeys] sortedArrayUsingSelector: #selector(compare:)];
// As a default, set the selected photo's CLLocation class
// variable to contain the first value in the sorted dictionary
self.selectedPhotoLocation = [self.locationDictionary objectForKey:[sortedKeys objectAtIndex:0]];
// Then go through the location dictionary and set the
// photo location to whatever value in the dictionary
// has a key containing a time most closely before
// the image timestamp. Note that the keys can be compared
// as strings since they're formatted in descending order --
// year to month to day to hour to minute to second.
for (NSString *key in sortedKeys) {
// If the photo's metadata timestamp is less than or equal to
// the current key, set the selected photo's location class
// variable to contain the CLLocation value associated with the key
if ([self.selectedPhotoDateTime compare:key] != NSOrderedAscending) {
self.selectedPhotoLocation = [self.locationDictionary objectForKey:key];
}
// Else if the time in the location dictionary is past
// the photo's timestamp, break from the loop
else {
break;
}
}
}
[self dismissViewControllerAnimated:YES completion:nil];
}
In your .h file you need to add the following to your code:
#interface TakePhotoViewController : UIViewController <CLLocationManagerDelegate>
In your .m file you need the following.
#interface TakePhotoViewController (){
//location stuff
CLLocationManager * manager;
CLGeocoder *geocoder;
CLPlacemark * placemark;
}
The above code set's up relevant references to find your location.
Next add this to your viewDidLoad method:
manager = [[CLLocationManager alloc] init];
geocoder = [[CLGeocoder alloc] init];
This initialises the Location Manager and Geocoder.
Then in your code that either initiates taking a picture or returns the picture to a view use this:
manager.delegate = self;
manager.desiredAccuracy = kCLLocationAccuracyBest;
[manager startUpdatingLocation];
To stop the continuous updating of the location add this to your imagePickerReturn method:
[manager stopUpdatingLocation];
As soon as you stop updating the location you will be saving the very last location that was updated. The location updates once every .5 - 1 second, so even if you are moving or have the camera open for a long time it will only store the location of whatever image you pick. To save the date (which includes time down to milliseconds) use:
NSDate * yourCoreDataDateName = [NSDate date];
For good coding practice to handle any errors you will need this:
//handles the error if location unavailable
- (void) locationManager:(CLLocationManager *)manager didFailWithError:(NSError *)error{
NSLog(#"Error: %#", error);
NSLog(#"Failed to get location... :-(");
}
- (void) locationManager:(CLLocationManager *)manager didUpdateToLocation:(CLLocation *)newLocation fromLocation:(CLLocation *)oldLocation{
NSLog(#"Location: %#", newLocation);
CLLocation * currentLocation = newLocation;
self.locationTF.text = #"Finding Location...";
if (currentLocation != nil){
[geocoder reverseGeocodeLocation:currentLocation completionHandler:^(NSArray *placemarks, NSError *error) {
if (error == nil && [placemarks count] > 0){
placemark = [placemarks lastObject];
//the code below translates the coordinates into readable text of the location
self.locationLabel.text = [NSString stringWithFormat:#"%# %# , %#, %#, %# %#", placemark.subThoroughfare, placemark.thoroughfare, placemark.locality, placemark.administrativeArea, placemark.country, placemark.postalCode];
}
else{
NSLog(#"%#", error.debugDescription);
self.locationLabel.text = #"Failed to find location";
}
}];
}
}
I must warn you that iOS8 will throw NO error when it can't find a location because it needs you as the programmer to add an alert view to authorise getting locations. See this tutorial on how to overcome this issue:
http://nevan.net/2014/09/core-location-manager-changes-in-ios-8/
I got this tutorial from the most popular asked question about the new error.
The most accurate way to do this would be through the exif metadata. Have a look at this post by Ole Begemann on how to do this.
UPDATE
It seems like Apple doesn't include the location to the metadata to images taken with the Camera from the UIImagePicker.
Another option to get the location in which the image was taken would be to use a custom overlay view for the UIImagePicker and get the location when the takePicture method is called. This would probably achieve the best result.
I am using saveEventually in the section of code below. The issue is, saveEventually doesn't work when the application is minimised and reopened, only when the application is completely closed and reopened. Is there any way to counteract this?
-(void)locationManager:(CLLocationManager *)manager didUpdateToLocation:(CLLocation *)newLocation fromLocation:(CLLocation *)oldLocation {
// Generate Random Number
int random = arc4random_uniform(1000000000);
// Updates Location
NSLog(#"Location: %#",newLocation);
CLLocation *curentLocation = newLocation;
if (curentLocation != nil) {
// Submit through Parse
PFObject *object = [PFObject objectWithClassName:#"Issue"];
object[#"ID"] = [NSString stringWithFormat:#"%i",random];
object[#"Latitude"] = [NSString stringWithFormat:#"%.8f",curentLocation.coordinate.latitude];
object[#"Longitude"] = [NSString stringWithFormat:#"%.8f",curentLocation.coordinate.longitude];
object[#"Signal"] = [NSString stringWithFormat:#"%#",_avgNumber];
[object saveEventually];
// Update Text Fields
self.latitude.text = [NSString stringWithFormat:#"%.8f",curentLocation.coordinate.latitude];
self.longitude.text = [NSString stringWithFormat:#"%.8f",curentLocation.coordinate.longitude];
self.signal.text = [NSString stringWithFormat:#"%#",_avgNumber];
}
// Stop the Location Update
[manager stopUpdatingLocation];
}
saveEventually will work under both of the conditions you described. From Parse: "Objects saved with this method will be stored locally in an on-disk cache until they can be delivered to Parse.
They will be sent immediately if possible. Otherwise, they will be sent the next time a network connection is available."
It should work, but as a temporary workaround if it's not, you could check for connectivity before saving the object. If you have a internet connection, use saveInBackground, if not use saveEventually. So something like this:
if (isNetworkAvailable)
[object saveInBackground];
else
[object saveEventually];
- (BOOL)isNetworkAvailable
{
CFNetDiagnosticRef dReference;
dReference = CFNetDiagnosticCreateWithURL (NULL, (__bridge CFURLRef)[NSURL URLWithString:#"www.apple.com"]);
CFNetDiagnosticStatus status;
status = CFNetDiagnosticCopyNetworkStatusPassively (dReference, NULL);
CFRelease (dReference);
if ( status == kCFNetDiagnosticConnectionUp )
{
NSLog (#"Connection is Available");
return YES;
}
else
{
NSLog (#"Connection is down");
return NO;
}
}
Due to the nature of my application (tracks signal) there is high probability that the application will be utilised when the device currently has no cellular signal.
How can I ensure that the method below will run once the phone recognises that it has signal?
I would still like the data to be posted but with no further interaction from the user after the initial action press that calls the method.
-(void)locationManager:(CLLocationManager *)manager didUpdateToLocation:(CLLocation *)newLocation fromLocation:(CLLocation *)oldLocation {
NSLog(#"Location: %#",newLocation);
CLLocation *curentLocation = newLocation;
if (curentLocation != nil) {
// Code below uses a third party utility to submit data (Parse framework)
PFObject *Object = [PFObject objectWithClassName:#"Issue"];
Object[#"Latitude"] = [NSString stringWithFormat:#"%.8f",curentLocation.coordinate.latitude];
Object[#"Longitude"] = [NSString stringWithFormat:#"%.8f",curentLocation.coordinate.longitude];
Object[#"Signal"] = [NSString stringWithFormat:#"%#",_avgNumber];
[Object saveInBackground];
// Update Text Fields
self.latitude.text = [NSString stringWithFormat:#"%.8f",curentLocation.coordinate.latitude];
self.longitude.text = [NSString stringWithFormat:#"%.8f",curentLocation.coordinate.longitude];
self.signal.text = [NSString stringWithFormat:#"%#",_avgNumber];
}
// Stop the Location Update
[manager stopUpdatingLocation];
}
Is there any way to capture the event occurs when a user connects to a particular WiFi network in iOS app. It is fine even if this can be achieved using any private library which doesn't require super user privileges (jail break). I just want to capture the changing event of the connected SSID.
I would recommend simply using what Larme posted, and setting up an NSTimer to check every second or so, what the SSID of your current network is, if you detect a change, simply do whatever you need to do. Keep in mind, changing WiFi networks is not something that happens instantaneously, so having a 1 second resolution is not bad
In applicationDidFinishLoading
NSTimer *ssidTimer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:#selector(fetchSSIDInfo) userInfo:nil repeats:YES];
In AppDelegate
- (id)fetchSSIDInfo {
NSArray *ifs = (__bridge_transfer id)CNCopySupportedInterfaces();
NSLog(#"Supported interfaces: %#", ifs);
id info = nil;
NSString *ifnam = #"";
for (ifnam in ifs) {
info = (__bridge_transfer id)CNCopyCurrentNetworkInfo((__bridge CFStringRef)ifnam);
NSLog(#"%# => %#", ifnam, info);
if (info && [info count]) { break; }
}
if ([info count] >= 1 && [ifnam caseInsensitiveCompare:prevSSID] != NSOrderedSame) {
// Trigger some event
prevSSID = ifnam;
}
return info;
}
Something like that. I can not check if code is typo free as I am not in front of a mac, but it should not be too different
You can fetch details from your wifi connection:
- (NSDictionary *)getConnectionDetails
{
NSDictionary *connectionDetails = [NSDictionary dictionary];
CFArrayRef myArray = CNCopySupportedInterfaces();
if (myArray) {
CFDictionaryRef myDict = CNCopyCurrentNetworkInfo(CFArrayGetValueAtIndex(myArray, 0));
connectionDetails = (__bridge_transfer NSDictionary*)myDict;
}
return connectionDetails;
}
And then if check [connectionDetails valueForKey:#"BSSID"] you will get BSSID.
Also please note that you must to import #import <SystemConfiguration/CaptiveNetwork.h>
You want SystemConfiguration, which has facilities for seeing notifications on all sorts of networking changes. In particular you'll want to use SCDynamicStoreSetNotificationKeys to listen for changes to the devices and SCNetworkConfiguration to get information about the available interfaces.
I have a simple locations map and I want to make my app beep when the user is approaching a location that is listed in a remote file
the listed locations are on my server named locations.txt
how can i check locations.txt every 1 minute to see if the user is within 300m of a location??
The standard answer to this question is Shape-Based Regions as described in the Location Awareness Guide. Generally, shape-based regions is the way to go if you have a limited number of regions. But, given that you want a lot of regions, you might have to "roll your own":
Turn on a location service and monitor your location. See the Location Awareness Programming Guide. If you use standard location service, make sure to set a desiredAccuracy that is as low as possible to achieve the functional need (e.g. kCLLocationAccuracyHundredMeters).
Once you've successfully received the first didUpdateLocations, if you really want to check every minute, you could create a timer at that point. If the purpose of that timer is to just check the users' location, then the timer is really not needed and you can just wait for occurrences of the didUpdateLocations.
You can iterate through your array of locations to monitor (I'd convert them to CLLocation objects) and simply use distanceFromLocation.
A couple of observations, though:
You suggest that you want to check locations.txt every minute to see if user is within 300m of location. I can imagine two reasons why you might have proposed that solution:
Did server's locations.txt change? If this is the problem you're trying to solve, a better solution would be push notifications (a.k.a. "remote notifications") and you want to make sure the client has access to the latest information. The process of constantly re-retrieving the file is very expensive (in terms of bandwidth, battery, computationally); or
Did the user move? If you're concerned about whether the user may have moved, the right solution is not to check every minute, but rather wait for the [CLLocationManagerDelegate] instance method didUpdateLocations to be called. If you want to avoid too many redundant checks, you can always keep track of whether the last request took place less than a minute ago or not, and then only check again if it was more than a minute ago. But that's very different than checking every minute whether you need to or not.
You've suggested using a text file. You might want to contemplate using a JSON file (or XML file), which is a better mechanism for retrieving data from a server.
For example, if you have a text file in JSON format, you can parse the results in another single line of code (JSONObjectWithData). To illustrate, let me show you what a JSON file might look like (where the square brackets designate an array, and the curly braces designate a dictionary, this is therefore an array of dictionaries):
[
{
"name" : "Battery Park",
"latitude" : 40.702,
"longitude" : -74.015
},
{
"name" : "Grand Central Station",
"latitude" : 40.753,
"longitude" : -73.977
}
]
Then your app can retrieve the results incredibly easily with two lines:
NSData *locationsData = [NSData dataWithContentsOfURL:url];
NSArray *locationsArray = [NSJSONSerialization JSONObjectWithData:locationsData options:0 error:&error];
So, you'll need to start location services:
if (nil == self.locationManager)
self.locationManager = [[CLLocationManager alloc] init];
self.locationManager.delegate = self;
self.locationManager.desiredAccuracy = kCLLocationAccuracyKilometer;
// Set a movement threshold for new events.
self.locationManager.distanceFilter = 500;
[self.locationManager startUpdatingLocation];
You'll then have a routine for checking the current location:
- (void)checkLocation
{
NSURL *url = [NSURL URLWithString:kLocationsUrlString];
NSData *locationsData = [NSData dataWithContentsOfURL:url];
NSAssert(locationsData, #"failure to download data"); // replace this with graceful error handling
NSError *error;
NSArray *locationsArray = [NSJSONSerialization JSONObjectWithData:locationsData
options:0
error:&error];
NSAssert(locationsArray, #"failure to parse JSON"); // replace with with graceful error handling
for (NSDictionary *locationEntry in locationsArray)
{
NSNumber *longitude = locationEntry[#"longitude"];
NSNumber *latitude = locationEntry[#"latitude"];
NSString *locationName = locationEntry[#"name"];
CLLocation *location = [[CLLocation alloc] initWithLatitude:[latitude doubleValue]
longitude:[longitude doubleValue]];
NSAssert(location, #"failure to create location");
CLLocationDistance distance = [location distanceFromLocation:self.locationManager.location];
if (distance <= 300)
{
NSLog(#"You are within 300 meters (actually %.0f meters) of %#", distance, locationName);
}
else
{
NSLog(#"You are not within 300 meters (actually %.0f meters) of %#", distance, locationName);
}
}
}
And this will be called when the user's location changes:
// this is used in iOS 6 and later
- (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations
{
[self checkLocation];
}
// this is used in iOS 5 and earlier
- (void)locationManager:(CLLocationManager *)manager didUpdateToLocation:(CLLocation *)newLocation fromLocation:(CLLocation *)oldLocation
{
if ([[[UIDevice currentDevice] systemVersion] floatValue] < 6.0)
[self checkLocation];
}
The implementation might look like this test project on GitHub. This is a barebones implementation, but it gives you an idea of the tools you have at hand, namely retrieving your locations.json file and comparing that to the location retrieved by the device.