3

I am trying to sort my tableview in ascending order by distance that I calculate from coordinates. Everything works like a charm except I can't get it in ascending order, I have been mucking around with NSSortDescriptor etc., but getting unlucky, any help would be appreciated, here is my code:

- (void) retrieveData
{
    NSURL *url = [NSURL URLWithString:jsonFile];
    NSData *data = [NSData dataWithContentsOfURL:url];

    _jsonArray = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:nil];

    _salesArray = [[NSMutableArray alloc]init];

    for (int i = 0; i < _jsonArray.count; i++) {

        NSString *sID = [[_jsonArray objectAtIndex:i] objectForKey:@"id"];
        NSString *sName = [[_jsonArray objectAtIndex:i] objectForKey:@"name"];
        NSString *sAddress = [[_jsonArray objectAtIndex:i] objectForKey:@"address"];
        NSString *sPostcode = [[_jsonArray objectAtIndex:i] objectForKey:@"postcode"];


        __block NSString *distance;
        CLGeocoder *geocoder = [[CLGeocoder alloc]init];
        [geocoder geocodeAddressString:sPostcode completionHandler:^(NSArray *placemarks,        NSError *error) {


            if (error == nil && placemarks.count > 0) {

                CLPlacemark *placemark = [placemarks objectAtIndex:0];


                CLLocation *location = placemark.location;
                CLLocation *myLocation = self.manager.location;
                CLLocationDistance miles =  [location distanceFromLocation:myLocation];
                //this is the variable i want in my convenience init.
                distance = [NSString stringWithFormat:@"%.1f m", (miles/1609.344)];
            }
        }];

        [_salesArray addObject:[[sales alloc] initWithSales:sID andName:sName andAddress:sAddress andPostcode:distance]];

    }

    [_salesArray sortUsingComparator:
     ^NSComparisonResult(id obj1, id obj2){
             sales *p1 = (sales *)obj1;
             sales *p2 = (sales *)obj2;
             if (p1.postcode > p2.postcode) {
                 return (NSComparisonResult)NSOrderedDescending;
             }

             if (p1.postcode < p2.postcode) {
                 return (NSComparisonResult)NSOrderedAscending;
             }
             return (NSComparisonResult)NSOrderedSame;
         }
     ];

     [self.tableView reloadData];
}
ansible
  • 3,569
  • 2
  • 18
  • 29
silly_cone
  • 137
  • 1
  • 8
  • I don't see where you're trying to sort. Also you should read the documentation on `geocodeAddressString:completionHandler:` because you are making a number of mistakes in using it. – Carl Veazey Jul 15 '14 at 00:50
  • You need to sort your `_salesArray` array so that the objects are in the correct order in the array when you populate your cells. Where is the code where you attempt to sort the array? – Robotic Cat Jul 15 '14 at 01:00
  • customCell.distance.text shows all the distances in the tableview except that its not in order, this is what i am trying to sort ascendingly, _salesArray contains names and postcodes, i am converting the postcodes to coordinates and then calculating the distances from my location, @CarlVeazey i will have a read on geocodeaddressString, there is no code there that attempts sorting the customCell.distance.text in ascending order, i was hoping you guys would help with that. cheers – silly_cone Jul 15 '14 at 01:05
  • We can't help you sort by distance if you only ever known the distance right before you show the cell. Fix that first and update your code. You said you were having trouble with `NSSortDescriptor`, once you fix your geocoding attempt you will probably have no trouble sorting them. – Carl Veazey Jul 15 '14 at 01:53
  • 2
    Why are you not sorting the array depends on distance with name out side of cellRowAtIndex & reload tabelview again? – Wagh Jul 15 '14 at 01:57
  • 1
    @silly_cone, You don't "sort" the cells of a table view. You have to sort the datasource (`_salesArray`) _before_ you give it to the table view. That also means you have to calculate the distances for each item in `_salesArray` _before_ you sort the array. –  Jul 15 '14 at 02:15
  • would someone mind sharing an example code snippet please, @Anna – silly_cone Jul 16 '14 at 01:07
  • @RoboticCat here is the rest of the code, please see if you can help, thanks. – silly_cone Jul 18 '14 at 20:49

2 Answers2

2

There are a few of issues here:

  1. The geocodeAddressString imposes a few limitations, as outlined in the documentation:

    This method submits the specified location data to the geocoding server asynchronously and returns. Your completion handler block will be executed on the main thread. After initiating a forward-geocoding request, do not attempt to initiate another forward- or reverse-geocoding request.

    Geocoding requests are rate-limited for each app, so making too many requests in a short period of time may cause some of the requests to fail. When the maximum rate is exceeded, the geocoder passes an error object with the value kCLErrorNetwork to your completion handler.

    Several key observations here:

    • This runs asynchronously (so you cannot call geocodeAddressString and use its results immediately afterwards). You have do invoke the work contingent on the geocoding inside the completion block.

    • You should not be starting the next geocode request until the prior one completes.

    This means that you have to geocode the first postal code, let it complete asynchronously (i.e. later), geocode the next one, let it complete, etc., and only then do your sort and reload the table. A simple for loop is not an appropriate way to do this. You can either write a method that does a single geocode and invokes the next geocode in the completion block, or you can use NSOperation subclass as I have below.

  2. I would advise storing the distance as a NSNumber. In MVC, the one decimal place string representation is a "view" behavior, and should probably not be part of the "model".

    The advantage of this is that when you want to sort the objects, you can simply invoke the compare method for the NSNumber. For example, if salesPersonnel was a NSMutableArray of objects which each SalesPerson object has the NSNumber property called distance, you could then do:

    [self.salesPersonnel sortUsingComparator:^NSComparisonResult(SalesPerson *obj1, SalesPerson *obj2) {
        return [obj1.distance compare:obj2.distance]; 
    }];
    

    I wasn't sure if your sales entries per actual sales transactions or sales personnel, so I apologize if I misinterpreted the object types, but hopefully this illustrates the idea.


You can do this any way you want, but for me, when I want to run a number of asynchronous tasks, but do so sequentially, I gravitate to concurrent NSOperation subclass which I'll add to a serial NSOperationQueue.

NSError *error;
NSArray *addressEntries = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
NSAssert(addressEntries, @"unable to parse: %@", error);

NSOperationQueue *queue = [[NSOperationQueue alloc] init];
queue.maxConcurrentOperationCount = 1;

self.salesPersonnel = [NSMutableArray array];

// define sort operation that will be called when all of the geocode attempts are done

NSOperation *sortAndReloadTableOperation = [NSBlockOperation blockOperationWithBlock:^{
    [self.salesPersonnel sortUsingComparator:^NSComparisonResult(SalesPerson *obj1, SalesPerson *obj2) {
        return [obj1.distance compare:obj2.distance];
    }];

    [self.tableView reloadData];
}];

// create the geocode operations

for (NSDictionary *addressEntry in addressEntries) {
    SalesPerson *salesPerson = [[SalesPerson alloc] initWithSalesId:addressEntry[@"id"]
                                                               name:addressEntry[@"name"]
                                                            address:addressEntry[@"address"]
                                                         postalCode:addressEntry[@"postcode"]];
    [self.salesPersonnel addObject:salesPerson];

    NSOperation *geocodeOperation = [[GeocodeOperation alloc] initWithPostalCode:salesPerson.postalCode completionHandler:^(NSArray *placemarks, NSError *error) {
        CLPlacemark *placemark = [placemarks firstObject];

        CLLocation *location = placemark.location;
        CLLocationDistance meters = [location distanceFromLocation:self.currentLocation];
        salesPerson.distance = @(meters / 1609.344);
    }];

    [sortAndReloadTableOperation addDependency:geocodeOperation];  // note, the final sort is dependent upon this finishing

    [queue addOperation:geocodeOperation];                         // go ahead and queue up the operation
}

// now we can queue the sort and reload operation, which won't start until the geocode operations are done

[[NSOperationQueue mainQueue] addOperation:sortAndReloadTableOperation];

And the GeocodeOperation is a basic concurrent NSOperation subclass:

//  GeocodeOperation.h

#import <Foundation/Foundation.h>

typedef void(^GeocodeCompletionHandler)(NSArray *placemarks, NSError *error);

@interface GeocodeOperation : NSOperation

@property (nonatomic, copy) GeocodeCompletionHandler geocodeCompletionHandler;

- (instancetype)initWithPostalCode:(NSString *)postalCode completionHandler:(GeocodeCompletionHandler)geocodeCompletionHandler;

@end

and the implementation (note, the main method is the only interesting bit here ... all the rest is routine concurrent NSOperation subclass code; personally, I move all of the concurrent NSOperation stuff into a base class, which cleans up this GeocodeOperation code, but I didn't want to confuse this further, so I've kept this simple):

//  GeocodeOperation.m

#import "GeocodeOperation.h"
@import CoreLocation;

@interface GeocodeOperation ()

@property (nonatomic, readwrite, getter = isFinished)  BOOL finished;
@property (nonatomic, readwrite, getter = isExecuting) BOOL executing;

@property (nonatomic, copy) NSString *postalCode;

@end

@implementation GeocodeOperation

@synthesize finished = _finished;
@synthesize executing = _executing;

- (CLGeocoder *)sharedGeocoder
{
    static CLGeocoder *geocoder = nil;

    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        geocoder = [[CLGeocoder alloc]init];
    });

    return geocoder;
}

- (instancetype)initWithPostalCode:(NSString *)postalCode completionHandler:(GeocodeCompletionHandler)geocodeCompletionHandler
{
    self = [super init];
    if (self) {
        _postalCode = [postalCode copy];
        _geocodeCompletionHandler = geocodeCompletionHandler;
    }
    return self;
}

- (void)main
{
    [[self sharedGeocoder] geocodeAddressString:self.postalCode completionHandler:^(NSArray *placemarks, NSError *error) {
        if (self.geocodeCompletionHandler) {
            self.geocodeCompletionHandler(placemarks, error);
        }

        [self completeOperation];
    }];
}

#pragma mark - NSOperation methods

- (void)start
{
    if ([self isCancelled]) {
        self.finished = YES;
        return;
    }

    self.executing = YES;

    [self main];
}

- (void)completeOperation
{
    self.executing = NO;
    self.finished = YES;
}

- (BOOL)isConcurrent
{
    return YES;
}

- (void)setExecuting:(BOOL)executing
{
    if (_executing != executing) {
        [self willChangeValueForKey:@"isExecuting"];
        _executing = executing;
        [self didChangeValueForKey:@"isExecuting"];
    }
}

- (void)setFinished:(BOOL)finished
{
    if (_finished != finished) {
        [self willChangeValueForKey:@"isFinished"];
        _finished = finished;
        [self didChangeValueForKey:@"isFinished"];
    }
}

@end
Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • thanks for this Rob, excellent breakdown, by the way do you know the daily limitations per app for geocodeAddressString, thanks. – silly_cone Jul 22 '14 at 14:06
  • No, I'm afraid I don't. I assume it's to avoid gross abuses (geocoding the entire white pages), but Apple is ambiguous on the point. You might want to make sure you handle geocoding errors gracefully, though. – Rob Jul 22 '14 at 14:20
0

I think the problem is postcode is an NSString. So in your block (p1.postcode > p2.postcode) is comparing the ADDRESS LOCATIONS, not the string values themselves.

You want to use the NSString function compare: instead of doing it yourself.

Try this:

[_salesArray sortUsingComparator:
 ^NSComparisonResult(id obj1, id obj2){

      sales *p1 = (sales *)obj1;
      sales *p2 = (sales *)obj2;
      NSString *postcode1 = p1.postcode;
      NSString *postcode2 = p2.postcode;

      return [postcode1 compare:posecode2];
 ];
ansible
  • 3,569
  • 2
  • 18
  • 29
  • the p1.postcode actually returns a floating point number, your method doesnt work properly, please show correct method, thanks. – silly_cone Jul 18 '14 at 22:05
  • Hmm, you are setting andPostcode with distance, which is a string... `[_salesArray addObject:[[sales alloc] initWithSales:sID andName:sName andAddress:sAddress andPostcode:distance]];` which is odd, but I guess I don't know what you are doing there. Is `geocodeAddressString:` asynchronous? Have you logged out _salesArray before you sort? Does it have the items you expect? Also, try debugging your block and see what happens. Good luck. – ansible Jul 19 '14 at 04:37