本文为《Core Data by Tutorials》笔记上篇,代码用 swift3 编写。等这系列写完会根据 objc 的《Core Data》 补充笔记,下面的代码只给其中关键部分,请指教。由于笔记是给自己看的,部分地方可能会跳跃性比较大。
funcsave(name: String) { guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { return } // 1 let managedContext = appDelegate.persistentContainer.viewContext // 2 let entity = NSEntityDescription.entity(forEntityName: "Person", in: managedContext)! let person = NSManagedObject(entity: entity, insertInto: managedContext) // 3 person.setValue(name, forKey: "name") // 4 do { try managedContext.save() people.append(person) } catch let error as NSError { print("Could not save./(error),/(error.userInfo)") } }
从 Core Data 中保存或恢复数据之前,都要用到 NSManagedObjectContext
。managed object context 就像一个内存「暂存器」用来处理 managed objects。
把一个新的 managed object 加进一个 managed object context,如果满意这些修改,我们可以直接在 managed object context 中「commit」 这些修改然后保存起来。
创建一个新的 managedObject 然后保存到 context 中。
// Save test bow tie let bowtie = NSEntityDescription.insertNewObject(forEntityName: "Bowtie", into: self.persistentContainer.viewContext) as! Bowtie bowtie.name = "My bow tie" bowtie.lastWorn = NSDate() // Retrieve test bow tie do { let request = NSFetchRequest<Bowtie>(entityName: "Bowtie") let ties = try self.persistentContainer.viewContext.fetch(request) let sample = ties.first print("Name:/(sample?.name), Worn:/(sample?.lastWorn)") } catch let error as NSError { print("Fetching error:/(error),/(error.userInfo)") }
// 在 viewDidLoad() 之前执行 override funcviewWillAppear(_animated: Bool) { super.viewWillAppear(animated) // 1 从 application delegate 中获取它 persistent container 的引用并得到 NSManagedObjectContext guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { return } let managedContext = appDelegate.persistentContainer.viewContext // 2 FetchRequest 可以有不同方式去获取数据 let fetchRequest = NSFetchRequest<NSManagedObject>(entityName: "Person") // 3 获取数据 do { people = try managedContext.fetch(fetchRequest) } catch let error as NSError { print("Could not fetch./(error),/(error.userInfo)") } }
之前通过获取应用 delegate 的 managed object context 来获得访问权限,现在可以把 managed object context 当做一个属性在类和类之间传送。
这样 ViewController 可以不需要知道它来自哪就使用它,链式传递 context,这样能使代码变得简洁。
AppDelegate.swift
var window: UIWindow? funcapplication(_application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { guard let vc = window?.rootViewController as? ViewController else { return true } vc.managedContext = persistentContainer.viewContext return true }
ViewController.swift
var managedContext: NSManagedObjectContext! // MARK: - View Life Cycle override funcviewDidLoad() { super.viewDidLoad() // 1 导入 plist 数据 insertSampleData() // 2 搜索条件 let request = NSFetchRequest<Bowtie>(entityName: "Bowtie") let firstTitle = segmentedControl.titleForSegment(at: 0)! request.predicate = NSPredicate(format: "searchKey == %@", firstTitle) do { // 3 获取数据 [Bowtie] let results = try managedContext.fetch(request) // 4 展示数据 populate(bowtie: results.first!) } catch let error as NSError { print("Counld not fetch/(error),/(error.userInfo)") } }
堆(Stack)由四个 Core Data 类组成:
清楚堆的工作是很有必要的,例如要从旧的数据库迁移数据。
NSPersistentStoreCoordinator 是 managed object model 和 persistent store 的桥梁。它理解 NSManagedObjectModel,也知道怎么去从 NSPersistentStore 传消息和获取消息。
NSPersistentStoreCoordinator 同时隐藏了实现 persistent store 或 stores 配置的细节,有两个原因:
日常使用中,你会经常使用 NSManagedObjectContext,可能只有在用 Core Data 使用一些更高级的功能时才会看到其他三个部分。
理解 context 如何工作也是很重要的:
save()
,所有的改动才会影响到储存卡中的数据。 更重要的还有:
let managedContext = employee.managedObjectContext
在 iOS 10 中,NSPersistentContainer 是一个新的类,它能管理所有四个 Core Data stack 类——the managed model, the store coordinator, the persistent store 和 managed context。
Dog Walk.xcdatamodeld
其中狗对遛狗这个行为是一对多的关系,而遛狗行为对狗而言是一对一的关系,如下图:
新建一个Core Data Stack
CoreDataStack.swift
import Foundation import CoreData classCoreDataStack{ private let modelName: String init(modelName: String) { self.modelName = modelName } // 只有这个不加 private 是因为 managed context 是 stack 的唯一的入口 lazy var managedContext: NSManagedObjectContext = { return self.storeContainer.viewContext }() private lazy var storeContainer: NSPersistentContainer = { // initialization let container = NSPersistentContainer(name: self.modelName) // 读取 persistent stores container.loadPersistentStores { (storeDescription, error) in if let error = error as NSError? { print("Unresolved error/(error),/(error.userInfo)") } } return container }() funcsaveContext() { guard managedContext.hasChanges else { return } do { try managedContext.save() } catch let error as NSError { print("Unresolved error/(error),/(error.userInfo)") } } }
AppDelegate.swift
import UIKit import CoreData @UIApplicationMain classAppDelegate:UIResponder,UIApplicationDelegate{ var window: UIWindow? lazy var coreDataStack = CoreDataStack(modelName: "Dog Walk") funcapplication(_application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { guard let navController = window?.rootViewController as? UINavigationController, let viewController = navController.topViewController as? ViewController else { return true } viewController.managedContext = coreDataStack.managedContext return true } // MARK: 进入后台前或终止前,应用会用CoreDataStack.swift 中的 saveContext() 保存数据变更 funcapplicationDidEnterBackground(_application: UIApplication) { coreDataStack.saveContext() } funcapplicationWillTerminate(_application: UIApplication) { coreDataStack.saveContext() } }
ViewController.swift
import UIKit import CoreData classViewController:UIViewController{ // MARK: - Properties lazy var dateFormatter: DateFormatter = { let formatter = DateFormatter() formatter.dateStyle = .short formatter.timeStyle = .medium return formatter }() var currentDog: Dog? var managedContext: NSManagedObjectContext! // MARK: - IBOutlets @IBOutlet var tableView: UITableView! // MARK: - View Life Cycle override funcviewDidLoad() { super.viewDidLoad() tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell") let dogName = "Fido" let dogFetch: NSFetchRequest<Dog> = Dog.fetchRequest() dogFetch.predicate = NSPredicate(format: "%K == %@", #keyPath(Dog.name), dogName) do { let results = try managedContext.fetch(dogFetch) if results.count > 0 { // Fido found, use Fido currentDog = results.first } else { // Fido not found, create Fido currentDog = Dog(context: managedContext) currentDog?.name = dogName try managedContext.save() } } catch let error as NSError { print("Fetch error:/(error)description:/(error.userInfo)") } } } // MARK: - IBActions extensionViewController{ @IBAction funcadd(_sender: UIBarButtonItem) { let walk = Walk(context: managedContext) walk.date = NSDate() // 把新的 Walk 插进 Dog's walks 中 // if let dog = currentDog, let walks = dog.walks?.mutableCopy() as? NSMutableOrderedSet { // walks.add(walk) // dog.walks = walks // } // 和上面注释的代码同样效果 currentDog?.addToWalks(walk) // 保存 do { try managedContext.save() } catch let error as NSError { print("Save error:/(error)description:/(error.userInfo)") } tableView.reloadData() } } // MARK: UITableViewDataSource extensionViewController:UITableViewDataSource{ functableView(_tableView: UITableView, numberOfRowsInSection section: Int) -> Int { guard let walks = currentDog?.walks else { return 1 } return walks.count } functableView(_tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) guard let walk = currentDog?.walks?[indexPath.row] as? Walk, let walkDate = walk.date as? Date else { return cell } cell.textLabel?.text = dateFormatter.string(from: walkDate) return cell } functableView(_tableView: UITableView, titleForHeaderInSection section: Int) -> String? { return "List of Walks" } } // 删除数据 functableView(_tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) { guard let walkToRemove = currentDog?.walks?[indexPath.row] as? Walk, editingStyle == .delete else { return } // managed context 中 删除数据 managedContext.delete(walkToRemove) do { try managedContext.save() //表中删除行 tableView.deleteRows(at: [indexPath], with: .automatic) } catch let error as NSError { print("Saving error:/(error), description:/(error.userInfo)") } } //tableView 左划删除 functableView(_tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { return true }
总结:
从 CoreDataStack.swift
可以看到,我们新建了个 Core Data Stack 来管理 context,其中 managed context 由初始化后的 NSPersistentContainer 类实例的 .viewContext
属性获取。另外 AppDelegate.swift
也用到 CoreDataStack.swift
中的 saveContext()
方法来保存数据变更。至此,我们完成通过 Stack 来对数据增删改查,第一阶段结束。
前面我们都是一下子获取所有搜索到的数据,这节讲的是如何更好地获取数据。
本节目标:
之前获取数据都是先创建一个 NSFetchRequest 实例,配置好搜索范围然后再在 context 上获取数据。但实际上,我们有五种不同的方法来实现操作。
// 1 let fetchRequest1 = NSFetchRequest<Venue>() let entity = NSEntityDescription.entity(forEntityName: "Venue", in: managedContext)! fetchRequest1.entity = entity // 2 第一种写法的缩写形式 let fetchRequest2 = NSFetchRequest<Venue>(entityName: "Venue") // 3 第二种写法的缩写形式 fetchRequest()方法被定义在 Venue+CoreDataProperties.swift let fetchRequest3: NSFetchRequest<Venue> = Venue.fetchRequest() // 4 从 NSManagedObjectModel 中获取数据 let fetchRequest4 = managedObjectModel.fetchRequestTemplate(forName: "venueFR") // 5 和第四种写法类似,但这里多了一些参数去限制获取结果。 let fetchRequest5 = managedObjectModel.fetchRequestFromTemplate(withName: "venueFR", substitutionVariables: ["NAME" : "Vivi Bubble Tea"])
NSFetchResult 不仅仅是一个简单的工具,实际上,它可以说是 Core Data 框架中的瑞士军刀。
你可以用它来获取单独的数据,对数据进行统计,例如:平均数、最小值、最大值等等。
NSFetchRequest 有个属性叫 resultType,
拿第二点 .countResultType
来说,有的人可能会直接获取所有的 managed objects 之后再调用数组的 count
属性得到 object 的数量。但是一旦要获取一个城市的人口数量的时候,先获取所有人口的 object 再得到数量这样显然对内存是很不友好的,这时候通过 .countResultType
获取结果的数量会更有效率。
例如,我想获得价格分类只有一个「$」的珍珠奶茶店数量,我们给 fetchRequest 配置好 resultType
结果类型属性,在配置好 predicate
查询范围后,就可以直接从 countResult.first!.intValue
得到。
var coreDataStack: CoreDataStack! lazy var cheapVenuePredicate: NSPredicate = { return NSPredicate(format: "%K == %@", #keyPath(Venue.priceInfo.priceCategory), "$") }() //... // 获取数量并配置 label funcpopulateCheapVenueCountLabel() { // 抓取的是数量 所以是 NSNumber let fetchRequest = NSFetchRequest<NSNumber>(entityName: "Venue") // 返回满足抓取要求的数据数量 fetchRequest.resultType = .countResultType // 只抓取一个 $ 的 fetchRequest.predicate = cheapVenuePredicate do { let countResult = try coreDataStack.managedContext.fetch(fetchRequest) // 获取数量 let count = countResult.first!.intValue firstPriceCategoryLabel.text = "/(count)bubble tea places" } catch let error as NSError { print("Could not fetch/(error),/(error.userInfo)") } }
当然我们还可以有不同的搜索数量的方式,这里我们获得价格分类有三个「$」的珍珠奶茶店数量。和以前一样先把搜索范围定位所有的 Venue object,然后在获取结果的时候点名只获取数量。
lazy var expensiveVenuePredicate: NSPredicate = { return NSPredicate(format: "%K == %@", #keyPath(Venue.priceInfo.priceCategory), "$$$") }() //... funcpopulateExpensiveVenueCountLabel() { // 构建获得 Venue object 的请求 let fetchRequest: NSFetchRequest<Venue> = Venue.fetchRequest() // 获得三个 $ 的 fetchRequest.predicate = expensiveVenuePredicate do { // 用 count 属性获取数量 let count = try coreDataStack.managedContext.count(for: fetchRequest) thirdPriceCategoryLabel.text = "/(count)bubble tea places" } catch let error as NSError { print("Could not fetch/(error),/(error.userInfo)") } }
第三点的 .dictionaryResultType
能能返回经过不同计算后的数据,同样的我们通过 NSExpression 来实现一些简单的计算。下图是 API 文档中的部分属性,供参考。
例如,我们要搜索所有珍珠奶茶的优惠数量,我们同样没有必要找出所有相关的属性再自己求和,Core Data 可以帮我们完成任务。
funcpopulateDealsCountLabel() { // 1 .dictionaryResultType 告诉 fetchRequest 要进行计算 let fetchRequest = NSFetchRequest<NSDictionary>(entityName: "Venue") fetchRequest.resultType = .dictionaryResultType // 2 创建一个 NSExpressionDescription 去请求求和后的数据,然后把这个请求过程的名字定为 sumDeals let sumExpressionDesc = NSExpressionDescription() sumExpressionDesc.name = "sumDeals" // 3 构建表达式 一开始说明要计算的数据来源是 specialCount(优惠的数量) // 然后说明计算方式为"sum:"求和,结果为 integer32AttributeType 类型 let specialCountExp = NSExpression(forKeyPath: #keyPath(Venue.specialCount)) sumExpressionDesc.expression = NSExpression(forFunction: "sum:", arguments: [specialCountExp]) sumExpressionDesc.expressionResultType = .integer32AttributeType // 4 配置 fetchRequest fetchRequest.propertiesToFetch = [sumExpressionDesc] // 5 返回字典类型数据,再按之前的名字取出对应的值 do { let results = try coreDataStack.managedContext.fetch(fetchRequest) let resultDict = results.first! let numDeals = resultDict["sumDeals"]! numDealsLabel.text = "/(numDeals)total deals" } catch let error as NSError { print("Could not fetch/(error),/(error.userInfo)") } }
剩下一个类型是 .managedObjectResultType
,当你用这类型去获取结果的时候,结果会是一个 NSManagedObjectID
组成的数组,而不是原来的 managed objects。一个 NSManagedObjectID
是一个managed object 的压缩的统一标识,作用就像数据库中的主键一样。
在 iOS 5,人们通常通过 ID 来获取数据,因为 NSManagedObjectID
是线程安全的,而且通过用它能帮助开发者实现并发线程限制模型(thread confinement concurrency model)。
现在线程限制对于很多并发模型来说已经过时了,通过 object ID 来获取数据的做法也在逐渐减少。
目前我们尝试过关于获取数据的不同方式,但是有时候我们需要限制获取的数据数量,我们有时没有必要去一次性获取所有对象图(object graph),这样对内存也不友好。
我们有不同方式去限制获取结果的数量,例如 NSFetchRequest
支持获取的批次数量 (fetching batches)。我们可以用 fetchBatchSize
、 fetchLimit
、 fetchOffset
去控制获取批次数量的行为。
Core Data 也尝试通过一种名叫「faulting」的技术去减少内存消耗,一个 fault 是一个用来表示 managed object 没有被完全送进内存的占位符。
最后,限制对象图的另外一种方法是用 predicates,就像之前做的一样。
NSFetchRequest 另外一个强大的功能就是能帮你排序好数据,它是通过 NSSortDescriptor 来实现的。这样的排序是在 SQLite 层面的,而不是在内存中,所以这让 Core Data 中的排序即快又有效率。
现在要实现根据珍珠奶茶店的名字升序、降序、距离、价格来排序,首先定义好 NSSortDescriptor。
// 按名字排序 lazy var nameSortDescriptor: NSSortDescriptor = { let compareSelector = #selector(NSString.localizedStandardCompare(_:)) return NSSortDescriptor(key: #keyPath(Venue.name), ascending: true, selector: compareSelector) }() // 按距离排序 lazy var distanceSortDescriptor: NSSortDescriptor = { return NSSortDescriptor(key: #keyPath(Venue.location.distance), ascending: true) }() // 按价格排序 lazy var priceSortDescriptor: NSSortDescriptor = { return NSSortDescriptor(key: #keyPath(Venue.priceInfo.priceCategory), ascending: true) }()
初始化一个 NSSortDescriptor 的实例需要做三件事:有一个 key path 去指出要排序的属性路径(数据库表中:表→属性,表→表→属性等等),要求升序或降序, 一个可选的选择器(option selector)去实现比较操作。
如果你之前用过 NSSortDescriptor,你可能知道有一种基于块的(block-based) API 可以把比较器(comparator)代替为选择器(seletor)。遗憾的是,Core Data 不支持通过这种方法来定义一个 sort descriptor。
同样的 Core Data 也不支持 NSPredicate 中基于块的(block-based)方法,原因是过滤和分类操作是在 SQLite 数据库中完成的,所以 predicate/sort descriptor 不得不去很好的匹配数据并写成 SQLite 语句。
另外, NSString.localizedStandardCompare(_:)
是苹果推荐用来根据符合当前语言环境(the current locale)的语言规则来排序,这可以更好地去处理一些特殊字符,例如 bien sûr :]
在 tableView(didSelectRowAt:)
方法中完成赋值,「Search」按钮事件为触发 ViewController.swift
中的委托方法。
FilterViewController.swift
/// 定义一个委托方法:当用户选择一个新的过滤操作时候(sort/filter combination),会通知委托。 protocolFilterViewControllerDelegate:class{ funcfilterViewController(filter: FilterViewController, didSelectPredicate predicate: NSPredicate?, sortDescriptor: NSSortDescriptor?) } classFilterViewController:UITableViewController{ //... } // MARK: - UITableViewDelegate extensionFilterViewController{ override functableView(_tableView: UITableView, didSelectRowAt indexPath: IndexPath) { guard let cell = tableView.cellForRow(at: indexPath) else { return } // Price section switch cell { //Sort By section case nameAZSortCell: selectedSortDescriptor = nameSortDescriptor case nameZASortCell: selectedSortDescriptor = nameSortDescriptor.reversedSortDescriptor as? NSSortDescriptor case distanceSortCell: selectedSortDescriptor = distanceSortDescriptor case priceSortCell: selectedSortDescriptor = priceSortDescriptor default: break } cell.accessoryType = .checkmark } } // MARK: - IBActions extensionFilterViewController{ @IBAction funcsaveButtonTapped(_sender: UIBarButtonItem) { delegate?.filterViewController(filter: self, didSelectPredicate: selectedPredicate, sortDescriptor: selectedSortDescriptor) dismiss(animated: true) } }
ViewController.swift 中补充委托方法。
// MARK: - FilterViewControllerDelegate extensionViewController:FilterViewControllerDelegate{ funcfilterViewController(filter: FilterViewController, didSelectPredicate predicate: NSPredicate?, sortDescriptor: NSSortDescriptor?) { fetchRequest.predicate = nil fetchRequest.sortDescriptors = nil fetchRequest.predicate = predicate if let sr = sortDescriptor { fetchRequest.sortDescriptors = [sr] } //获取数据并 reload tableView fetchAndReload() } }
当你看到这里,我好消息和坏消息要告诉你。好消息是我们已经说了很多关于 NSFetchRequest 可以做的事,坏消息是我们每次获取数据都会屏蔽主线程,直到获取到数据。
当你屏蔽了主线程,屏幕就会变得不可交互,还会产生一些其他的问题,之前没有感觉到屏蔽主线程的感觉,是因为我们获取的数据还太少。iOS 8 中,Core Data 有一个 API 能让我们长时间在后台获取数据,获取到数据后还能得到一个回调方法。
// 不先初始化的话 回调方法会报错 var venues: [Venue] = [] // 父类是 NSPersistentStoreRequest 而不是 NSFetchRequest var asyncFetchRequest: NSAsynchronousFetchRequest<Venue>! // MARK: - View Life Cycle override funcviewDidLoad() { super.viewDidLoad() // 1 准备获取数据 fetchRequest = Venue.fetchRequest() // 2 用 fetchRequest 和回调完成请求,数据在 result.finalResult 中 asyncFetchRequest = NSAsynchronousFetchRequest<Venue>(fetchRequest: fetchRequest) { [unowned self] (result: NSAsynchronousFetchResult) in guard let venues = result.finalResult else { return } self.venues = venues self.tableView.reloadData() } // 3 执行异步请求 do { try coreDataStack.managedContext.execute(asyncFetchRequest) } catch let error as NSError { print("Could not fetch/(error),/(error.userInfo)") } }
还要注意的是要获取的 venues 实例,由于现在获取数据是异步的,所以获取数据的步骤会在 table view 初始化之后再执行,所以要先初始化好实例,不然不能解包实例,应用会报错。
另外,如果要取消获取数据的请求(fetch request),可以调用 NSAsynchronousFetchResult
的 cancel()
方法。
有时候我们需要从 Core Data 中获取数据是去改变一个单独的属性(attribute),改动后,我们还要去 commit 回 persistent store。但如果我们想要去一次性更新十万计的数据呢?这将会消耗大量的时间和内存去只更新一个属性。
iOS 8 中,有一个新的方法能不从内存中获取所有数据来完成更新数据:batch updates。这新的技术能绕过 NSManagedObjectContext 来直接操作 persistent store。通常批量更新的做法就像邮件客户端中的「Mark all as read」功能一样。
// MARK: - View Life Cycle override funcviewDidLoad() { super.viewDidLoad() let batchUpdate = NSBatchUpdateRequest(entityName: "Venue") batchUpdate.propertiesToUpdate = [#keyPath(Venue.favorite) : true] batchUpdate.affectedStores = coreDataStack.managedContext .persistentStoreCoordinator?.persistentStores batchUpdate.resultType = .updatedObjectsCountResultType do { let batchResult = try coreDataStack.managedContext .execute(batchUpdate) as! NSBatchUpdateResult print("Records updated/(batchResult.result!)") } catch let error as NSError { print("Could not fetch/(error),/(error.userInfo)") } }
运行应用后显示: Records updated 30
iOS 9 中,NSBatchDeleteRequest 能帮我批量删除数据,如批量更新一样,不需要把数据读取到内存再操作,而且父类也是 NSPersistentStoreRequest。
我们在回避 NSManagedObjectContext,所以批量更新或批量删除时,我们不会进行数据验证。数据的改动不会影响我们的 managed context,所以在用一个 persistent store request 之前要验证好数据。
之前我们都是把 Core Data 和 UITableView 放在一起用,Core Data,提供了一个类来专门处理这种使用方式:NSFetchedResultsController。NSFetchedResultsController 是一个 controller,但是它不是一个 view controller,它没有界面,它的目的在于帮助开发者通过抽象大部分代码更容易地在 table view 上同步数据。
下面的代码是关于一个世界杯胜场计数的应用示例。
var fetchedResultsController: NSFetchedResultsController<Team>! // MARK: - View Life Cycle override funcviewDidLoad() { super.viewDidLoad() // 1 fetchRequest 是万能的 let fetchRequest: NSFetchRequest<Team> = Team.fetchRequest() // 2 初始化 fetchedResultsController fetchedResultsController = NSFetchedResultsController( fetchRequest: fetchRequest, managedObjectContext: coreDataStack.managedContext, sectionNameKeyPath: nil, cacheName: nil) // 3 开始获取数据 do { try fetchedResultsController.performFetch() } catch let error as NSError { print("Fetching error:/(error),/(error.userInfo)") } }
这里有点奇怪的是 NSFetchedResultsController 没有返回什么值就可以获取数据了,其实 NSFetchedResultsController 既是 fetch request 的包装,也是一个获取数据用的 container,我们可以从中获取到数据。例如我们可以通过 fetchedObject
属性或 object(at:)
方法来获得。
// MARK: - Internal extensionViewController{ funcconfigure(cell: UITableViewCell,forindexPath: IndexPath) { guard let cell = cell as? TeamCell else { return } let team = fetchedResultsController.object(at: indexPath) cell.flagImageView.image = UIImage(named: team.imageName!) cell.teamLabel.text = team.teamName cell.scoreLabel.text = "Wins:/(team.wins)" } } // MARK: - UITableViewDataSource extensionViewController:UITableViewDataSource{ // section 数 funcnumberOfSections(intableView: UITableView) -> Int { guard let sections = fetchedResultsController.sections else { return 0 } return sections.count } functableView(_tableView: UITableView, numberOfRowsInSection section: Int) -> Int { guard let sectionInfo = fetchedResultsController.sections?[section] else { return 0 } return sectionInfo.numberOfObjects } functableView(_tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: teamCellIdentifier, for: indexPath) configure(cell: cell, for: indexPath) return cell } } // MARK: - UITableViewDelegate extensionViewController:UITableViewDelegate{ //点击后胜场加一,保存数据到 Core Data,并重载 tableview functableView(_tableView: UITableView, didSelectRowAt indexPath: IndexPath) { let team = fetchedResultsController.object(at: indexPath) team.wins = team.wins + 1 coreDataStack.saveContext() tableView.reloadData() } }
到了这步,启动应用的话还会报 'An instance of NSFetchedResultsController requires a fetch request with sort descriptors'
的错,因为我们使用 NSFetchedResultsController,它需要我们给它提供至少一个 sort descriptor,才能知道如何整理数据。与之前不一样的是,前面获取数据的时候可以不提供 sort descriptor。
于是在之前的基础上加上 sort descriptor,这里同时增加了三种排序。
// MARK: - View Life Cycle override funcviewDidLoad() { super.viewDidLoad() // 1 fetchRequest 是万能的 let fetchRequest: NSFetchRequest<Team> = Team.fetchRequest() // 必须提供至少一个 sort descriptor let zoneSort = NSSortDescriptor(key: #keyPath(Team.qualifyingZone), ascending: true) let scoreSort = NSSortDescriptor(key: #keyPath(Team.wins), ascending: false) let nameSort = NSSortDescriptor(key: #keyPath(Team.teamName), ascending: true) fetchRequest.sortDescriptors = [zoneSort, scoreSort, nameSort] // 2 初始化 fetchedResultsController fetchedResultsController = NSFetchedResultsController( fetchRequest: fetchRequest, managedObjectContext: coreDataStack.managedContext, sectionNameKeyPath: nil, cacheName: nil) // 3 开始获取数据 do { try fetchedResultsController.performFetch() } catch let error as NSError { print("Fetching error:/(error),/(error.userInfo)") } }
如果在前面 viewDidLoad()
方法中更改下 fetchedResultsController 的 sectionNameKeyPath,就可以直接按照相关的字符串分类,例如这里把国家按照大洲分类。注意前面的 sort descriptor 也要加上相应的分类,例如 let zoneSort = NSSortDescriptor(key: #keyPath(Team.qualifyingZone), ascending: true)
,否则分类顺序会错乱。
// 2 初始化 fetchedResultsController fetchedResultsController = NSFetchedResultsController( fetchRequest: fetchRequest, managedObjectContext: coreDataStack.managedContext, sectionNameKeyPath: #keyPath(Team.qualifyingZone), cacheName: nil)
再增加一个委托方法提供 section 的标题:
functableView(_tableView: UITableView, titleForHeaderInSection section: Int) -> String? { let sectionInfo = fetchedResultsController.sections?[section] return sectionInfo?.name }
NSFetchedResultsController 提供了缓存功能,只要在之前的 viewDidLoad()
方法中更改下 fetchedResultsController
的 cacheName 就能实现了。
fetchedResultsController = NSFetchedResultsController( fetchRequest: fetchRequest, managedObjectContext: coreDataStack.managedContext, sectionNameKeyPath: #keyPath(Team.qualifyingZone), cacheName: "worldCup")
我们要注意缓存是把数据缓存到硬盘中,和 Core Data 的 persistent store 是分开的。如果我们要更改获取的数据,或者不一样的 sort descriptor 等等导致缓存无效的时候,我们必须用 deleteCache(withName:)
删除现有缓存,或者换一个缓存名。
前面更新数据的方法就是调用 table view 的 reloadData()
方法,其实 NSFetchedResultsController 直接给我们提供了委托方法,让我们可以在数据改动的时候直接更新 table view。
// MARK: - NSFetchedResultsControllerDelegate extensionViewController:NSFetchedResultsControllerDelegate{ // 数据将会改变,调用 beginUpdates() 方法 funccontrollerWillChangeContent(_controller: NSFetchedResultsController<NSFetchRequestResult>) { tableView.beginUpdates() } // 球队相关数据改变 funccontroller(_controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?,fortype: NSFetchedResultsChangeType, newIndexPath: IndexPath?) { switch type { case .insert: tableView.insertRows(at: [newIndexPath!], with: .automatic) case .delete: tableView.deleteRows(at: [indexPath!], with: .automatic) case .update: let cell = tableView.cellForRow(at: indexPath!) as! TeamCell configure(cell: cell, for: indexPath!) case .move: tableView.deleteRows(at: [indexPath!], with: .automatic) tableView.insertRows(at: [newIndexPath!], with: .automatic) } } // 数据完成改变,应用变化。 funccontrollerDidChangeContent(_controller: NSFetchedResultsController<NSFetchRequestResult>) { tableView.endUpdates() } // section 相关数据改变 funccontroller(_controller: NSFetchedResultsController<NSFetchRequestResult>, didChange sectionInfo: NSFetchedResultsSectionInfo, atSectionIndex sectionIndex: Int,fortype: NSFetchedResultsChangeType) { let indexSet = IndexSet(integer: sectionIndex) switch type { case .insert: tableView.insertSections(indexSet, with: .automatic) case .delete: tableView.deleteSections(indexSet, with: .automatic) default: break } } }