如何将 NSFetchedResultsController 的部分 sectionNameKeyPath 设置为属性的第一个字母,而不仅仅是 Swift 中的属性,而不是 ObjC

How can I set NSFetchedResultsController's section sectionNameKeyPath to be the first letter of a attribute not just the attribute in Swift, NOT ObjC

我正在 swift 中重写一个旧的 obj-c 项目,它有一个带有 sectionIndex

的 tableView

我设置了一个谓词,因此它只 returns 个具有相同国家/地区属性的对象

我想根据国家属性的首字母制作栏目索引

在 obj-c 中,我创建了这样的 fetchedResultsController

_fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest
                                             managedObjectContext:self.managedObjectContext 
      sectionNameKeyPath:@"name.stringGroupByFirstInitial"                                                  cacheName:nil];

我在 NSString 上有一个扩展

  @implementation NSString (Indexing)

- (NSString *)stringGroupByFirstInitial {

    if (!self.length || self.length == 1)
        return self;

    return [self substringToIndex:1];
}

这与这两种方法结合使用效果很好

- (NSArray *) sectionIndexTitlesForTableView: (UITableView *) tableView{

    return [self.fetchedResultsController sectionIndexTitles];
}
- (NSInteger)tableView:(UITableView *)tableView sectionForSectionIndexTitle:(NSString *)title atIndex:(NSInteger)index{

    return [self.fetchedResultsController sectionForSectionIndexTitle:title atIndex:index];
}

在 swift 中,我尝试创建一个类似的扩展,名称更简洁 :)

extension String {
    func firstCharacter()-> String {
        let startIndex = self.startIndex
        let first =  self[...startIndex]
        return String(first)
    }
}

这在 playground 中工作正常,返回您调用它的任何字符串的第一个字符的字符串。

但在 swift 中使用类似的方法创建 fetchedResultsController,...

let fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest,
                                                          managedObjectContext: (dataModel?.container.viewContext)!,
                                                          sectionNameKeyPath: "country.firstCharacter()",
                                                          cacheName: nil)

...导致异常。这是控制台日志

2018-02-23 11:41:20.762650-0800 Splash[5287:459654] *** 由于未捕获的异常 'NSUnknownKeyException' 而终止应用程序,原因:'[valueForUndefinedKey:]: 这个 class 与键 firstCharacter() 的键值编码不兼容。'

任何建议作为解决此问题的正确方法将不胜感激

这不是所建议问题的重复,因为这与如何在 Swift 中实现这一点特别相关。我在另一个问题

中添加了在 Obj -c 中实现此目的的简单正确方法

加一个函数给你的DiveSiteclass到return第一个字母country:

@objc func countryFirstCharacter() -> String {
    let startIndex = country.startIndex
    let first = country[...startIndex]
    return String(first)
}

然后使用该函数名称(不带 ())作为 sectionNameKeyPath:

let fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest,
                                  managedObjectContext: (dataModel?.container.viewContext)!,
                                  sectionNameKeyPath: "countryFirstCharacter",
                                  cacheName: nil)

请注意,为了使 (Swift) 函数对 (Objective-C) FRC 可见,这里需要 @objc。 (遗憾的是,您不能只将 @objc 添加到您的字符串扩展中。)

最快的解决方案(适用于 Swift 4)

pbasdf 的解决方案有效。它比使用瞬态 属性 快得多。 我有大约 700 个名称的列表要显示在表视图中,并且从 CoreData 中获取相对复杂。 只是进行初步搜索:

  • 瞬态 属性 返回 'name' 的首字母大写:加载需要 2-3 秒。有些峰值长达 4 秒!
  • 使用 pbasdf 的解决方案(@obj func nameFirstCharacter() -> String,这个加载时间下降到 0.4 - 0.5s。
  • 最快的解决方案仍然是向模型添加常规 属性 'nameFirstChar'。并确保它已编入索引。在这种情况下,同一提取查询的加载时间下降到 0.01 - 0.02 秒!

我必须同意我同意一些评论,即向模型添加 属性 看起来有点脏,只是为了在表视图中创建部分。但考虑到性能提升,我会坚持使用该选项!

对于最快的解决方案,这是 Person+CoreDataProperties.swift 中的改编实现,因此在更改名称 属性 时会自动更新 namefirstchar。请注意,此处关闭了代码自动生成。 :

  //@NSManaged public var name: String? //removed default
  public var name: String? //rewritten to set namefirstchar correctly
  {
    set (new_name){
      self.willChangeValue(forKey: "name")
      self.setPrimitiveValue(new_name, forKey: "name")
      self.didChangeValue(forKey: "name")

      let namefirstchar_s:String = new_name!.substring(to: 1).capitalized
      if self.namefirstchar ?? "" != namefirstchar_s {
        self.namefirstchar = namefirstchar_s
      }
    }
    get {
      self.willAccessValue(forKey: "name")
      let text = self.primitiveValue(forKey: "name") as? String
      self.didAccessValue(forKey: "name")
      return text
    }
  }
  @NSManaged public var namefirstchar: String?

如果您不喜欢或不能添加新的 属性,或者如果您希望保持代码自动生成,这是我从 pbasdf 调整的解决方案:

1) 扩展 NSString

extension NSString{
  @objc func firstUpperCaseChar() -> String{ // @objc is needed to avoid crash
    if self.length == 0 {
      return ""
    }
    return self.substring(to: 1).capitalized
  }
}

2) 进行排序,无需使用排序键name.firstUpperCaseChar

let fetchRequest =   NSFetchRequest<NSFetchRequestResult>(entityName: "Person")
let personNameSort = NSSortDescriptor(key: "name", ascending: true, selector: #selector(NSString.caseInsensitiveCompare))
let personFirstNameSort = NSSortDescriptor(key: "firstname", ascending: true, selector: #selector(NSString.caseInsensitiveCompare))
fetchRequest.sortDescriptors = [personNameSort, personFirstNameSort]

3) 然后用右边的sectionNameKeyPath

进行fetch
fetchRequest.predicate = ... whatever you need to filter on ...
fetchRequest.fetchBatchSize = 50 //or 20 to speed things up
let fetchedResultsController = NSFetchedResultsController(   
  fetchRequest: fetchRequest,   
  managedObjectContext: managedObjectContext,   
  sectionNameKeyPath: "name.firstUpperCaseChar", //note no ()   
  cacheName: nil)

fetchedResultsController.delegate = self

let start = DispatchTime.now() // Start time
do {
  try fetchedResultsController.performFetch()
} catch {
  fatalError("Failed to initialize FetchedResultsController: \(error)")
}
let end = DispatchTime.now()   // End time
let nanoTime = end.uptimeNanoseconds - start.uptimeNanoseconds // time difference in nanosecs 
let timeInterval = Double(nanoTime) / 1_000_000_000 
print("Fetchtime: \(timeInterval) seconds")