Core Data 包含了如何让UITableView,UICo

作者: 星光社的戴铭 | 来源:发表于2015-09-06 18:01 被阅读631次

    包含组件

    最底层File System -> SQLite -> NSPersistent Store(可有多个) -> NSPersistent StoreCoordinator -> NSManagedObjectContext(可有多个,每个可包含多个NSManagedObject)

    设置堆栈

    范例:https://github.com/objcio/issue-4-full-core-data-application

    - (void)setupManagedObjectContext
    {
         //使用initWithConcurrencyType:来明确使用的是基于队列的并发模型
         self.managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
         self.managedObjectContext.persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:self.managedObjectModel];
         NSError* error;
         [self.managedObjectContext.persistentStoreCoordinator
              addPersistentStoreWithType:NSSQLiteStoreType
              configuration:nil
              URL:self.storeURL
              options:nil
              error:&error];
         if (error) {
              NSLog(@"error: %@", error);
         }
         self.managedObjectContext.undoManager = [[NSUndoManager alloc] init];
    }
    

    创建模型

    在xcode新建的Core Data选项中选择Data Model template,模型文件会被编译成.momd文件。模型创建完毕就可以创建与之对应的NSManagedObject子类。从菜单选择Editor > NSManagedObject subclass。

    模型的属性

    • 默认/可选:建议不使用带默认值的可选属性
    • Transient:方便撤销操作和故障处理,建议使用transient属性
    • 索引:提高读取速度
    • 标量类型:默认NSNumber,也可以使用int64_t,float_t或BOOL。

    创建Store类

    存储类除了managed object context还有rootItem方法,程序启动时会查找这root item然后传给root view controller。

    - (Item*)rootItem
    {
         NSFetchRequest* request = [NSFetchRequest fetchRequestWithEntityName:@"Item"];
         request.predicate = [NSPredicate predicateWithFormat:@"parent = %@", nil];
         NSArray* objects = [self.managedObjectContext executeFetchRequest:request error:NULL];
         Item* rootItem = [objects lastObject];
         if (rootItem == nil) {
              rootItem = [Item insertItemWithTitle:nil
                   parent:nil
                   inManagedObjectContext:self.managedObjectContext];
         }
         return rootItem;
    }
    
    //增加一个item
    + (instancetype)insertItemWithTitle:(NSString*)title
         parent:(Item*)parent
         inManagedObjectContext:(NSManagedObjectContext *)managedObjectContext
    {
         NSUInteger order = parent.numberOfChildren;
         Item* item = [NSEntityDescription insertNewObjectForEntityForName:self.entityName
              inManagedObjectContext:managedObjectContext];
         item.title = title;
         item.parent = parent;
         item.order = @(order);
         return item;
    }
    
    //获得子节点数量
    - (NSUInteger)numberOfChildren
    {
         return self.children.count;
    }
    
    //创建一个fetched results controller的方法方便自动更新table view
    - (NSFetchedResultsController*)childrenFetchedResultsController
    {
         NSFetchRequest* request = [NSFetchRequest fetchRequestWithEntityName:[self.class entityName]];
         request.predicate = [NSPredicate predicateWithFormat:@"parent = %@", self];
         request.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"order" ascending:YES]];
         return [[NSFetchedResultsController alloc] initWithFetchRequest:request
              managedObjectContext:self.managedObjectContext
              sectionNameKeyPath:nil
              cacheName:nil];
    }
    

    和Table View无缝结合

    创建一个NSFetchedResultsController作为table view的data source

    - (id)initWithTableView:(UITableView*)tableView
    {
         self = [super init];
         if (self) {
              self.tableView = tableView;
              self.tableView.dataSource = self;
         }
         return self;
    }
    
    - (void)setFetchedResultsController:(NSFetchedResultsController*)fetchedResultsController
    {
         _fetchedResultsController = fetchedResultsController;
         fetchedResultsController.delegate = self;
         [fetchedResultsController performFetch:NULL];
    }
    
    - (NSInteger)numberOfSectionsInTableView:(UITableView*)tableView
    {
         return self.fetchedResultsController.sections.count;
    }
    
    - (NSInteger)tableView:(UITableView*)tableView numberOfRowsInSection:(NSInteger)sectionIndex
    {
         id<NSFetchedResultsSectionInfo> section = self.fetchedResultsController.sections[sectionIndex];
         return section.numberOfObjects;
    }
    
    - (UITableViewCell*)tableView:(UITableView*)tableView
    cellForRowAtIndexPath:(NSIndexPath*)indexPath
    {
         id object = [self.fetchedResultsController objectAtIndexPath:indexPath];
         id cell = [tableView dequeueReusableCellWithIdentifier:self.reuseIdentifier
              forIndexPath:indexPath];
         [self.delegate configureCell:cell withObject:object];
         return cell;
    }
    

    创建Table View Controller

    在新建的Table view的viewDidLoad里写:

    fetchedResultsControllerDataSource = [[FetchedResultsControllerDataSource alloc] initWithTableView:self.tableView];
    self.fetchedResultsControllerDataSource.fetchedResultsController = self.parent.childrenFetchedResultsController;
    fetchedResultsControllerDataSource.delegate = self;
    fetchedResultsControllerDataSource.reuseIdentifier = @"Cell";
    

    实现delegate

    - (void)configureCell:(id)theCell withObject:(id)object
    {
         UITableViewCell* cell = theCell;
         Item* item = object;
         cell.textLabel.text = item.title;
    }
    

    添加

    在textFieldShouldReturn:里

    [Item insertItemWithTitle:title
              parent:self.parent
              inManagedObjectContext:self.parent.managedObjectContext];
         textField.text = @"";
         [textField resignFirstResponder];
    

    增删改后table view也会更改显示

    - (void)controller:(NSFetchedResultsController*)controller
         didChangeObject:(id)anObject
         atIndexPath:(NSIndexPath*)indexPath
         forChangeType:(NSFetchedResultsChangeType)type
         newIndexPath:(NSIndexPath*)newIndexPath
    {
         if (type == NSFetchedResultsChangeInsert) {
              [self.tableView insertRowsAtIndexPaths:@[newIndexPath]
                   withRowAnimation:UITableViewRowAnimationAutomatic];
         }
    }
    
    - (void)controllerWillChangeContent:(NSFetchedResultsController*)controller
    {
         [self.tableView beginUpdates];
    }
    
    - (void)controllerDidChangeContent:(NSFetchedResultsController*)controller
    {
         [self.tableView endUpdates];
    }
    

    和Collection View的结合

    范例:https://github.com/AshFurrow/UICollectionView-NSFetchedResultsController collection view没有beginUpdates和endUpdates方法,所以只能用performBatchUpdate方法收集所有更新,然后在controllerDidChangeContent中用block执行所有更新。

    如何传递Table view里的Model对象到新的view controller中

    - (void)prepareForSegue:(UIStoryboardSegue*)segue sender:(id)sender
    {
         [super prepareForSegue:segue sender:sender];
         if ([segue.identifier isEqualToString:selectItemSegue]) {
              [self presentSubItemViewController:segue.destinationViewController];
         }
    }
    
    - (void)presentSubItemViewController:(ItemViewController*)subItemViewController
    {
         Item* item = [self.fetchedResultsControllerDataSource selectedItem];
         subItemViewController.parent = item;
    }
    
    - (void)viewWillAppear:(BOOL)animated
    {
         [super viewWillAppear:animated];
         self.fetchedResultsControllerDataSource.paused = NO;
    }
    
    - (void)viewWillDisappear:(BOOL)animated
    {
         [super viewWillDisappear:animated];
         self.fetchedResultsControllerDataSource.paused = YES;
    }
    
    - (void)setPaused:(BOOL)paused
    {
         _paused = paused;
         if (paused) {
              self.fetchedResultsController.delegate = nil;
         } else {
              self.fetchedResultsController.delegate = self;
              [self.fetchedResultsController performFetch:NULL];
              [self.tableView reloadData];
         }
    }
    

    删除

    //让table view支持滑动删除
    - (BOOL)tableView:(UITableView*)tableView
    canEditRowAtIndexPath:(NSIndexPath*)indexPath
    {
         return YES;
    }
    
    - (void)tableView:(UITableView *)tableView
         commitEditingStyle:(UITableViewCellEditingStyle)editingStyle
         forRowAtIndexPath:(NSIndexPath *)indexPath {
         if (editingStyle == UITableViewCellEditingStyleDelete) {
              id object = [self.fetchedResultsController objectAtIndexPath:indexPath];
              [self.delegate deleteObject:object];
         }
    }
    
    //强制order变化,可以重写prepareForDeletion方法
    - (void)prepareForDeletion
    {
         NSSet* siblings = self.parent.children;
         NSPredicate* predicate = [NSPredicate predicateWithFormat:@"order > %@", self.order];
         NSSet* siblingsAfterSelf = [siblings filteredSetUsingPredicate:predicate];
         [siblingsAfterSelf enumerateObjectsUsingBlock:^(Item* sibling, BOOL* stop)
         {
              sibling.order = @(sibling.order.integerValue - 1);
         }];
    }
    

    增加删除的动画效果

    ...
    else if (type == NSFetchedResultsChangeDelete) {
         [self.tableView deleteRowsAtIndexPaths:@[indexPath]
              withRowAnimation:UITableViewRowAnimationAutomatic];
    }
    

    增加晃动撤销功能

    //第一步告诉application支持这个
    application.applicationSupportsShakeToEdit = YES;
    //重写UIResponder类中的两个方法
    - (BOOL)canBecomeFirstResponder {
         return YES;
    }
    
    - (NSUndoManager*)undoManager
    {
         return self.managedObjectContext.undoManager;
    }
    
    //在持续化stack中设置一个undo manager
    self.managedObjectContext.undoManager = [[NSUndoManager alloc] init];
    
    //实现上面几步后晃动时会得到两个按钮的提醒框,可以给让用户体验更加友好些
    NSString* title = textField.text;
    NSString* actionName = [NSString stringWithFormat:NSLocalizedString(@"add item \"%@\"", @"Undo action name of add item"), title];
    [self.undoManager setActionName:actionName];
    [self.store addItem:title parent:nil];
    

    排序

    可以参考官方文档:https://developer.apple.com/library/ios/documentation/CoreData/Reference/NSFetchedResultsControllerDelegate_Protocol/Reference/Reference.html#//apple_ref/doc/uid/TP40008228-CH1-SW14

    保存

    Fetch获取对象

    基础

    官方文档:https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/CoreData/Articles/cdFetching.html

    范例

    request.result = [NSPredicate predicateWithFormat:
         @"(%@ <= longitude) AND (longitude <= %@)"
         @"AND (%@ <= latitude) AND (latitude <= %@)",
         @(minLongitude), @(maxLongitude), @(minLatitude), @(maxLatitude)];
    //取消将值放到row cache中。
    request.returnsObjectsAsFaults = NO;
    request.fetchLimit = 200;
    //执行fetch
    NSError *error = nil;
    NSArray *stops = [moc executeFetchRequest:request error:&error];
    NSAssert(stops != nil, @"Failed to execute %@: %@", request, error);
    //二次遍历
    NSPredicate *exactPredicate = [self exactLatitudeAndLongitudePredicateForCoordinate:self.location.coordinate];
    stops = [stops filteredArrayUsingPredicate:exactPredicate];
    
    - (NSPredicate *)exactLatitudeAndLongitudePredicateForCoordinate:(CLLocationCoordinate2D)pointOfInterest;
    {
         return [NSPredicate predicateWithBlock:^BOOL(Stop *evaluatedStop, NSDictionary *bindings) {
              CLLocation *evaluatedLocation = [[CLLocation alloc] initWithLatitude:evaluatedStop.latitude           longitude:evaluatedStop.longitude];
              CLLocationDistance distance = [self.location distanceFromLocation:evaluatedLocation];
              return (distance < self.distance);
         }];
    }
    
    //子查询
    NSPredicate *timePredicate = [NSPredicate predicateWithFormat:@"(%@ <= departureTime) && (departureTime <= %@)”, startDate, endDate];
    
    NSPredicate *predicate = [NSPredicate predicateWithFormat:
         @"(SUBQUERY(stopTimes, $x, (%@ <= $x.departureTime) && ($x.departureTime <= %@)).@count != 0)”, startDate, endDate];
    
    //文本搜索
    NSString *searchString = @"U Görli";
    predicate = [NSPredicate predicateWithFormat:@"name BEGINSWITH %@", searchString];
    
    

    导入大量数据

    导入应用Bundle里的SQLite文件

    NSFileManager* fileManager = [NSFileManager defaultManager];
    NSError *error;
    
    if([fileManager fileExistsAtPath:self.storeURL.path]) {
         NSURL *storeDirectory = [self.storeURL URLByDeletingLastPathComponent];
         NSDirectoryEnumerator *enumerator = [fileManager enumeratorAtURL:storeDirectory
              includingPropertiesForKeys:nil
              options:0
              errorHandler:NULL];
         NSString *storeName = [self.storeURL.lastPathComponent stringByDeletingPathExtension];
         //遍历目录下是否有重复
         for (NSURL *url in enumerator) {
              if (![url.lastPathComponent hasPrefix:storeName]) continue;
              [fileManager removeItemAtURL:url error:&error];
         }
          // 处理错误
    }
    
    NSString* bundleDbPath = [[NSBundle mainBundle] pathForResource:@"seed" ofType:@"sqlite"];
    [fileManager copyItemAtPath:bundleDbPath toPath:self.storeURL.path error:&error];
    
    //真机删除会失效,所以使用版本号来进行区分新旧
    NSString* bundleVersion = [infoDictionary objectForKey:(NSString *)kCFBundleVersionKey];
    NSString *seedVersion = [[NSUserDefaults standardUserDefaults] objectForKey@"SeedVersion"];
    if (![seedVersion isEqualToString:bundleVersion]) {
         // 复制源数据库
    }
    
    // ... 导入成功后
    NSDictionary *infoDictionary = [NSBundle mainBundle].infoDictionary;
    [[NSUserDefaults standardUserDefaults] setObject:bundleVersion forKey:@"SeedVersion"];
    

    导入范例

    https://github.com/objcio/issue-4-importing-and-fetching

    版本迁移

    Mapping Models

    NSMigrationManager能够推断两个版本模型的映射关系,但是如果版本跨度大了就力不从心了。

    Progressive Migrations渐进式迁移

    实现原理是两个版本之间确保正常,升级时按照一个版本一个版本渐进式的升级方式,比如最新的版本是第四版,如果用户使用的是第二版的,那么升级是就是先从第二版升级到第三版,然后再从第三版升级到第四版。完整范例:https://github.com/objcio/issue-4-core-data-migration 主要代码来自Marcus Zarrahttps://twitter.com/mzarra ,他的书关于Core Data的值得一看,http://pragprog.com/book/mzcd2/core-data

    迁移策略

    NSEntityMigrationPolicy这个类不光能够修改Entity的属性和关系,还能够自定义一些操作完成每个Entity的迁移。例如在Entity Mapping的Custom Polity里写上自定义的polity的方法

    NSNumber *modelVersion = [mapping.userInfo valueForKey:@"modelVersion"];
    if (modelVersion.integerValue == 2) {
         NSMutableArray *sourceKeys = [sourceInstance.entity.attributesByName.allKeys mutableCopy];
         NSDictionary *sourceValues = [sourceInstance dictionaryWithValuesForKeys:sourceKeys];
         NSManagedObject *destinationInstance = [NSEntityDescription insertNewObjectForEntityForName:mapping.destinationEntityName
              inManagedObjectContext:manager.destinationContext];
         NSArray *destinationKeys = destinationInstance.entity.attributesByName.allKeys;
         for (NSString *key in destinationKeys) {
              id value = [sourceValues valueForKey:key];
              // 避免value为空
              if (value && ![value isEqual:[NSNull null]]) {
                   [destinationInstance setValue:value forKey:key];
              }
         }
    }
    
    NSMutableDictionary *authorLookup = [manager lookupWithKey:@"authors"];
    // 检查该作者是否已经被创建了
    NSString *authorName = [sourceInstance valueForKey:@"author"];
    NSManagedObject *author = [authorLookup valueForKey:authorName];
    if (!author) {
         // 创建作者
         // ...
    
         // 更新避免重复
         [authorLookup setValue:author forKey:authorName];
    }
    [destinationInstance performSelector:@selector(addAuthorsObject:) withObject:author];
    
    //源存储和目的存储之间的关系
    [manager associateSourceInstance:sourceInstance
         withDestinationInstance:destinationInstance
         forEntityMapping:mapping];
    return YES;
    

    NSmigrationManager的category方法

    @implementation NSMigrationManager (Lookup)
    
    - (NSMutableDictionary *)lookupWithKey:(NSString *)lookupKey
    {
         NSMutableDictionary *userInfo = (NSMutableDictionary *)self.userInfo;
         // 这里检查一下是否已经建立了 userInfo 的字典
         if (!userInfo) {
              userInfo = [@{} mutableCopy];
              self.userInfo = userInfo;
         }
         NSMutableDictionary *lookup = [userInfo valueForKey:lookupKey];
         if (!lookup) {
              lookup = [@{} mutableCopy];
              [userInfo setValue:lookup forKey:lookupKey];
         }
         return lookup;
    }
    
    @end
    

    更复杂的迁移

    NSArray *users = [sourceInstance valueForKey:@"users"];
    for (NSManagedObject *user in users) {
    
         NSManagedObject *file = [NSEntityDescription insertNewObjectForEntityForName:@"File"
              inManagedObjectContext:manager.destinationContext];
         [file setValue:[sourceInstance valueForKey:@"fileURL"] forKey:@"fileURL"];
         [file setValue:destinationInstance forKey:@"book"];
    
         NSInteger userId = [[user valueForKey:@"userId"] integerValue];
         NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"User"];
         request.predicate = [NSPredicate predicateWithFormat:@"userId = %d", userId];
         NSManagedObject *user = [[manager.destinationContext executeFetchRequest:request error:nil] lastObject];
         [file setValue:user forKey:@"user"];
    }
    

    数据量大时的迁移改造,利用CoreData提供的chunks数据块方式。官方文档https://developer.apple.com/library/ios/documentation/cocoa/Conceptual/CoreDataVersioning/Articles/vmCustomizing.html#//apple_ref/doc/uid/TP40004399-CH8-SW9

    NSArray *mappingModels = @[mappingModel]; // 我们之前建立的那个模型
    if ([self.delegate respondsToSelector:@selector(migrationManager:mappingModelsForSourceModel:)]) {
         NSArray *explicitMappingModels = [self.delegate migrationManager:self
              mappingModelsForSourceModel:sourceModel];
         if (0 < explicitMappingModels.count) {
              mappingModels = explicitMappingModels;
         }
    }
    for (NSMappingModel *mappingModel in mappingModels) {
         didMigrate = [manager migrateStoreFromURL:sourceStoreURL
              type:type
              options:nil
              withMappingModel:mappingModel
              toDestinationURL:destinationStoreURL
              destinationType:type
              destinationOptions:nil
              error:error];
    }
    
    - (NSArray *)migrationManager:(MHWMigrationManager *)migrationManager
         mappingModelsForSourceModel:(NSManagedObjectModel *)sourceModel
    {
         NSMutableArray *mappingModels = [@[] mutableCopy];
         NSString *modelName = [sourceModel mhw_modelName];
         if ([modelName isEqual:@"Model2"]) {
              // 把该映射模型加入数组
         }
         return mappingModels;
    }
    
    - (NSString *)mhw_modelName
    {
         NSString *modelName = nil;
         NSArray *modelPaths = // get paths to all the mom files in the bundle
         for (NSString *modelPath in modelPaths) {
              NSURL *modelURL = [NSURL fileURLWithPath:modelPath];
              NSManagedObjectModel *model = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL];
              if ([model isEqual:self]) {
                   modelName = modelURL.lastPathComponent.stringByDeletingPathExtension;
                   break;
              }
         }
         return modelName;
    }
    

    建立单元测试,

    - (void)setUpCoreDataStackMigratingFromStoreWithName:(NSString *)name
    {
         NSURL *storeURL = [self temporaryRandomURL];
         [self copyStoreWithName:name toURL:storeURL];
    
         NSURL *momURL = [[NSBundle mainBundle] URLForResource:@"Model" withExtension:@"momd"];
         self.managedObjectModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:momURL];
    
         NSString *storeType = NSSQLiteStoreType;
    
         MHWMigrationManager *migrationManager = [MHWMigrationManager new];
         [migrationManager progressivelyMigrateURL:storeURL
              ofType:storeType
              toModel:self.managedObjectModel
              error:nil];
    
         self.persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:self.managedObjectModel];
         [self.persistentStoreCoordinator addPersistentStoreWithType:storeType
              configuration:nil
              URL:storeURL
              options:nil
              error:nil];
    
         self.managedObjectContext = [[NSManagedObjectContext alloc] init];
         self.managedObjectContext.persistentStoreCoordinator = self.persistentStoreCoordinator;
    }
    
    - (NSURL *)temporaryRandomURL
    {
         NSString *uniqueName = [NSProcessInfo processInfo].globallyUniqueString;
         return [NSURL fileURLWithPath:[NSTemporaryDirectory() stringByAppendingString:uniqueName]];
    }
    
    - (void)copyStoreWithName:(NSString *)name toURL:(NSURL *)url
    {
         // 每次创建一个唯一的url以保证测试正常运行
         NSBundle *bundle = [NSBundle bundleForClass:[self class]];
         NSFileManager *fileManager = [NSFileManager new];
         NSString *path = [bundle pathForResource:[name stringByDeletingPathExtension] ofType:name.pathExtension];
         [fileManager copyItemAtPath:path
              toPath:url.path error:nil];
    }
    
    //在测试类中复用
    - (void)setUp
    {
         [super setUp];
         [self setUpCoreDataStackMigratingFromStoreWithName:@"Model1.sqlite"];
    }
    

    调试迁移一个有用的启动参数是-com.apple.CoreData.MigrationDebug,设置1就会在console收到迁移数据时会出现的特殊的情况的信息。如果设置-com.apple.CoreData.SQLDebug 为 1还能够在console看到实际操作的SQL语句。

    Core Data的并行处理

    性能测试

    • 加上-com.apple.CoreData.SQLDebug1 作为启动参数传递给应用程序可以得到的输出
    sql: SELECT 0, t0.Z_PK, t0.Z_OPT, t0.ZIDENTIFIER, t0.ZLATITUDE, t0.ZLONGITUDE, t0.ZNAME FROM ZSTOP t0 WHERE (? <= t0.ZLONGITUDE AND t0.ZLONGITUDE <= ? AND ? <= t0.ZLATITUDE AND t0.ZLATITUDE <= ?) LIMIT 100
    annotation: sql connection fetch time: 0.0008s
    annotation: total fetch execution time: 0.0013s for 15 rows.
    

    实际生成的SQL是:

    SELECT 0, t0.Z_PK, t0.Z_OPT, t0.ZIDENTIFIER, t0.ZLATITUDE, t0.ZLONGITUDE, t0.ZNAME FROM ZSTOP t0
    WHERE (? <= t0.ZLONGITUDE AND t0.ZLONGITUDE <= ? AND ? <= t0.ZLATITUDE AND t0.ZLATITUDE <= ?)
    LIMIT 200
    
    % cd TrafficSearch
    % sqlite3 transit-data.sqlite
    SQLite version 3.7.13 2012-07-17 17:46:21
    Enter ".help" for instructions
    Enter SQL statements terminated with a ";"
    sqlite> EXPLAIN QUERY PLAN SELECT 0, t0.Z_PK, t0.Z_OPT, t0.ZIDENTIFIER, t0.ZLATITUDE, t0.ZLONGITUDE, t0.ZNAME FROM ZSTOP t0
    ...> WHERE (13.30845219672199 <= t0.ZLONGITUDE AND t0.ZLONGITUDE <= 13.33441458422844 AND 52.42769566863058 <= t0.ZLATITUDE AND t0.ZLATITUDE <= 52.44352370653525)
    ...> LIMIT 100;
    0|0|0|SEARCH TABLE ZSTOP AS t0 USING INDEX ZSTOP_ZLONGITUDE_INDEX (ZLONGITUDE>? AND ZLONGITUDE<?) (~6944 rows)
    

    输出

    0|0|0|SEARCH TABLE ZSTOP AS t0 USING INDEX ZSTOP_ZLONGITUDE_ZLATITUDE (ZLONGITUDE>? AND ZLONGITUDE<?) (~6944 rows)
    

    相关文章

      网友评论

        本文标题:Core Data 包含了如何让UITableView,UICo

        本文链接:https://www.haomeiwen.com/subject/qdadcttx.html