【问题标题】:Sort UITableView by distance按距离对 UITableView 进行排序
【发布时间】:2014-07-14 23:50:08
【问题描述】:

我正在尝试按从坐标计算的距离按升序对表格视图进行排序。一切都像一个魅力,除了我无法按升序获得它,我一直在用 NSSortDescriptor 等,但不幸的是,任何帮助将不胜感激,这是我的代码:

- (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];
}

【问题讨论】:

  • 我看不到您要在哪里进行排序。此外,您应该阅读geocodeAddressString:completionHandler: 上的文档,因为您在使用它时犯了许多错误。
  • 您需要对_salesArray 数组进行排序,以便在填充单元格时对象在数组中的顺序正确。您尝试对数组进行排序的代码在哪里?
  • customCell.distance.text 显示了 tableview 中的所有距离,除了它不是按顺序排列的,这是我要按升序排序的,_salesArray 包含名称和邮政编码,我正在将邮政编码转换为坐标然后计算与我所在位置的距离,@CarlVeazey 我将阅读 geocodeaddressString,那里没有尝试按升序对 customCell.distance.text 进行排序的代码,我希望你们能对此有所帮助。欢呼
  • 为什么不对数组进行排序取决于名称在 cellRowAtIndex 之外的距离并再次重新加载 tableview?
  • @silly_cone,您不会“排序”表格视图的单元格。您必须对数据源 (_salesArray) 将其提供给表格视图进行排序。这也意味着您必须在对数组进行排序之前计算_salesArray 中每个项目的距离。

标签: ios uitableview sorting nssortdescriptor


【解决方案1】:

这里有几个问题:

  1. geocodeAddressString 施加了一些限制,如文档中所述:

    该方法将指定的位置数据异步提交给地理编码服务器并返回。您的完成处理程序块将在主线程上执行。发起正向地理编码请求后,请勿尝试发起另一个正向或反向地理编码请求。

    每个应用的地理编码请求都有速率限制,因此在短时间内发出过多请求可能会导致某些请求失败。当超过最大速率时,地理编码器会将值为 kCLErrorNetwork 的错误对象传递给您的完成处理程序。

    这里有几个关键观察:

    • 这是异步运行的(因此您不能调用 geocodeAddressString 并在之后立即使用其结果)。您确实调用了依赖于完成块内部的地理编码的工作。

    • 在前一个地理编码请求完成之前,您不应开始下一个地理编码请求。

    这意味着您必须对第一个邮政编码进行地理编码,让它异步完成(即稍后),对下一个进行地理编码,让它完成等等,然后才进行排序并重新加载表格。一个简单的for 循环不是执行此操作的合适方法。您可以编写一个执行单个地理编码并在完成块中调用下一个地理编码的方法,也可以使用NSOperation 子类,如下所示。

  2. 我建议将distance 存储为NSNumber。在 MVC 中,一位小数的字符串表示是一种“视图”行为,可能不应该是“模型”的一部分。

    这样做的好处是,当您想要对对象进行排序时,您可以简单地为NSNumber 调用compare 方法。例如,如果 salesPersonnel 是一个 NSMutableArray 对象,每个 SalesPerson 对象都具有称为 distanceNSNumber 属性,那么您可以这样做:

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

    我不确定您的 sales 条目是否与实际销售交易或销售人员对应,因此如果我误解了对象类型,我深表歉意,但希望这能说明这个想法。


您可以按照自己的方式执行此操作,但对我来说,当我想运行多个异步任务但按顺序执行时,我倾向于并发NSOperation 子类,我将添加到串行@987654337 @。

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];

GeocodeOperation 是一个基本的并发NSOperation 子类:

//  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

和实现(注意,main 方法是这里唯一有趣的部分......所有其余的都是例行并发 NSOperation 子类代码;就个人而言,我将所有并发 NSOperation 东西移动到一个基础类,它清理了这个 GeocodeOperation 代码,但我不想进一步混淆,所以我保持这个简单):

//  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,出色的细分,顺便说一下,您知道 geocodeAddressString 的每个应用程序的每日限制,谢谢。
  • 不,恐怕我不知道。我认为这是为了避免严重滥用(对整个白页进行地理编码),但苹果在这一点上模棱两可。不过,您可能希望确保优雅地处理地理编码错误。
【解决方案2】:

我认为问题在于邮政编码是NSString。所以在你的块中(p1.postcode &gt; p2.postcode) 是比较地址位置,而不是字符串值本身。

你想使用 NSString 函数compare: 而不是自己做。

试试这个:

[_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];
 ];

【讨论】:

  • p1.postcode实际上返回的是一个浮点数,你的方法不能正常工作,请给出正确的方法,谢谢。
  • 嗯,你正在用距离设置 andPostcode,这是一个字符串......[_salesArray addObject:[[sales alloc] initWithSales:sID andName:sName andAddress:sAddress andPostcode:distance]]; 这很奇怪,但我想我不知道你在做什么。 geocodeAddressString: 是异步的吗?您在排序之前是否已注销 _salesArray?它有你期望的项目吗?另外,尝试调试你的块,看看会发生什么。祝你好运。