在前几期中,我们讨论了批处理更新和批处理删除。 在本教程中,我们将仔细研究如何实现异步提取以及在什么情况下您的应用程序可以从此新API中受益。
1.问题
像批处理更新一样,异步获取已在许多开发人员的心愿单上出现了一段时间。 提取请求可能很复杂,需要很短的时间才能完成。 在这段时间内,获取请求将阻塞正在其上运行的线程,因此,将阻止对执行获取请求的托管对象上下文的访问。 问题很容易理解,但是Apple的解决方案是什么样的。
2.解决方案
苹果对这个问题的答案是异步获取。 异步获取请求在后台运行。 这意味着它在执行时不会阻止其他任务,例如更新主线程上的用户界面。
异步获取还具有其他两个方便的功能,即进度报告和取消。 可以在任何时候取消异步获取请求,例如,当用户确定获取请求花费太长时间才能完成时。 进度报告是一项有用的功能,可以向用户显示获取请求的当前状态。
异步获取是一种灵活的API。 不仅可以取消异步获取请求,还可以在执行异步获取请求时更改托管对象的上下文。 换句话说,当应用程序在后台执行异步获取请求时,用户可以继续使用您的应用程序。
3.它如何运作?
与批处理更新一样,异步获取请求也作为NSPersistentStoreRequest
对象( NSAsynchronousFetchRequest
是NSAsynchronousFetchRequest
类的实例)传递到托管对象上下文。
NSAsynchronousFetchRequest
实例使用NSFetchRequest
对象和完成块初始化。 当异步获取请求完成其获取请求时,将执行完成块。
让我们重新访问本系列前面创建的待办事项应用程序 ,并用异步获取请求替换NSFetchedResultsController
类的当前实现。
步骤1:专案设定
从GitHub下载或克隆该项目,然后在Xcode 7中打开它。在开始使用NSAsynchronousFetchRequest
类之前,我们需要进行一些更改。 由于NSFetchedResultsController
类旨在在主线程上运行,因此我们将无法使用NSFetchedResultsController
类来管理表视图的数据。
步骤2:替换“提取的结果”控制器
首先,如下所示更新ViewController
类。 我们删除fetchedResultsController
属性,并创建一个类型为[Item]
的新属性items
,用于存储待办事项。 这也意味着ViewController
类不再需要符合NSFetchedResultsControllerDelegate
协议。
import UIKit
import CoreData
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
let ReuseIdentifierToDoCell = "ToDoCell"
@IBOutlet weak var tableView: UITableView!
var managedObjectContext: NSManagedObjectContext!
var items: [NSManagedObject] = []
...
}
在重构viewDidLoad()
方法之前,我首先要更新UITableViewDataSource
协议的实现。 看看我所做的更改。
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return items.count
}
func configureCell(cell: ToDoCell, atIndexPath indexPath: NSIndexPath) {
// Fetch Record
let record = items[indexPath.row]
// Update Cell
if let name = record.valueForKey("name") as? String {
cell.nameLabel.text = name
}
if let done = record.valueForKey("done") as? Bool {
cell.doneButton.selected = done
}
cell.didTapButtonHandler = {
if let done = record.valueForKey("done") as? Bool {
record.setValue(!done, forKey: "done")
}
}
}
func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
if (editingStyle == .Delete) {
// Fetch Record
let record = items[indexPath.row]
// Delete Record
managedObjectContext.deleteObject(record)
}
}
我们还需要在prepareForSegue(_:sender:)
方法中更改一行代码,如下所示。
// Fetch Record
let record = items[indexPath.row]
最后但并非最不重要的一点是,删除NSFetchedResultsControllerDelegate
协议的实现,因为我们不再需要它。
步骤3:创建异步提取请求
如下所示,我们在视图控制器的viewDidLoad()
方法中创建异步获取请求。 让我们花一点时间看看发生了什么。
override func viewDidLoad() {
super.viewDidLoad()
// Initialize Fetch Request
let fetchRequest = NSFetchRequest(entityName: "Item")
// Add Sort Descriptors
fetchRequest.sortDescriptors = [NSSortDescriptor(key: "createdAt", ascending: true)]
// Initialize Asynchronous Fetch Request
let asynchronousFetchRequest = NSAsynchronousFetchRequest(fetchRequest: fetchRequest) { (asynchronousFetchResult) -> Void in
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.processAsynchronousFetchResult(asynchronousFetchResult)
})
}
do {
// Execute Asynchronous Fetch Request
let asynchronousFetchResult = try managedObjectContext.executeRequest(asynchronousFetchRequest)
print(asynchronousFetchResult)
} catch {
let fetchError = error as NSError
print("\(fetchError), \(fetchError.userInfo)")
}
}
我们首先创建并配置NSFetchRequest
实例以初始化异步提取请求。 异步获取请求将在后台执行此获取请求。
// Initialize Fetch Request
let fetchRequest = NSFetchRequest(entityName: "Item")
// Add Sort Descriptors
fetchRequest.sortDescriptors = [NSSortDescriptor(key: "createdAt", ascending: true)]
要初始化NSAsynchronousFetchRequest
实例,我们调用init(request:completionBlock:)
,传入fetchRequest
和一个完成块。
// Initialize Asynchronous Fetch Request
let asynchronousFetchRequest = NSAsynchronousFetchRequest(fetchRequest: fetchRequest) { (asynchronousFetchResult) -> Void in
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.processAsynchronousFetchResult(asynchronousFetchResult)
})
}
当异步获取请求完成执行其获取请求时,将调用完成块。 完成块采用NSAsynchronousFetchResult
类型的一个参数,该参数包含查询结果以及对原始异步获取请求的引用。
在完成模块中,我们调用processAsynchronousFetchResult(_:)
,并传入NSAsynchronousFetchResult
对象。 稍后,我们将介绍这种辅助方法。
执行异步提取请求几乎与我们执行NSBatchUpdateRequest
相同。 我们在托管对象上下文上调用executeRequest(_:)
,传入异步获取请求。
do {
// Execute Asynchronous Fetch Request
let asynchronousFetchResult = try managedObjectContext.executeRequest(asynchronousFetchRequest)
print(asynchronousFetchResult)
} catch {
let fetchError = error as NSError
print("\(fetchError), \(fetchError.userInfo)")
}
即使异步获取请求是在后台执行的,也请注意executeRequest(_:)
方法会立即返回,从而为我们提供了一个NSAsynchronousFetchResult
对象。 异步获取请求完成后,将使用获取请求的结果填充相同的NSAsynchronousFetchResult
对象。
请记住,在上一教程中 , executeRequest(_:)
是一种抛出方法。 我们在do-catch
语句的catch
子句中捕获任何错误,并将它们打印到控制台进行调试。
步骤4:处理异步提取结果
processAsynchronousFetchResult(_:)
方法只不过是一个帮助器方法,在该方法中我们处理异步提取请求的结果。 我们使用结果的finalResult
属性的内容设置视图控制器的items
属性,然后重新加载表视图。
func processAsynchronousFetchResult(asynchronousFetchResult: NSAsynchronousFetchResult) {
if let result = asynchronousFetchResult.finalResult {
// Update Items
items = result as! [NSManagedObject]
// Reload Table View
tableView.reloadData()
}
}
步骤5:建立并执行
生成项目并在iOS Simulator中运行应用程序。 如果您的应用程序在尝试执行异步获取请求时崩溃,那么您可能正在使用从iOS 9(和OS X El Capitan)开始不推荐使用的API。
在Core Data和Swift:并发中 ,我解释了托管对象上下文可以具有的不同并发类型。 从iOS 9(和OS X El Capitan)开始,不建议使用ConfinementConcurrencyType
。 NSManagedObjectContext
类的init()
方法也是如此,因为它创建并发类型为ConfinementConcurrencyType
的实例。
如果您的应用程序崩溃,则很可能使用托管对象上下文和ConfinementConcurrencyType
并发类型,该类型不支持异步获取。 幸运的是,解决方案很简单。 使用指定的初始化程序init(concurrencyType:)
创建托管对象上下文,并传入MainQueueConcurrencyType
或PrivateQueueConcurrencyType
作为并发类型。
4.显示进度
NSAsynchronousFetchRequest
类增加了对监视提取请求进度的支持,甚至有可能取消异步提取请求,例如,如果用户认为要花费太长时间才能完成。
NSAsynchronousFetchRequest
类利用NSProgress
类进行进度报告以及取消异步获取请求。 自iOS 7和OS X Mavericks以来可用的NSProgress
类是一种监视任务进度的聪明方法,而无需将任务紧密耦合到用户界面。
NSProgress
类还支持取消,这是可以取消异步获取请求的方式。 让我们找出实现异步获取请求的进度报告所需要做的事情。
步骤1:添加SVProgressHUD
我们将使用Sam Vermette的SVProgressHUD库向用户显示异步获取请求的进度 。 完成此操作的最简单方法是通过CocoaPods 。 这就是项目的Podfile的样子。
source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '9.0'
use_frameworks!
pod 'SVProgressHUD', '~> 1.1'
从命令行运行pod install
,不要忘记打开CocoaPods为您创建的工作区,而不是打开Xcode项目。
步骤2:设定NSProgress
在本文中,我们不会详细探讨NSProgress
类,但可以随时在Apple文档中阅读有关它的更多信息。 在执行异步提取请求之前,我们在视图控制器的viewDidLoad()
方法中创建一个NSProgress
实例。
// Create Progress
let progress = NSProgress(totalUnitCount: 1)
// Become Current
progress.becomeCurrentWithPendingUnitCount(1)
我们将总单位数设置为1
可能会让您感到惊讶。 原因很简单。 当Core Data执行异步提取请求时,它不知道它将在持久性存储中找到多少记录。 这也意味着我们将无法向用户显示相对进度-一个百分比。 相反,我们将向用户显示绝对进度-找到的记录数。
您可以通过在执行异步获取请求之前执行获取请求以获取记录数来解决此问题。 我不愿意这样做,因为这也意味着从持久性存储中获取记录需要花费更长的时间才能完成,因为开始时会有额外的获取请求。
步骤3:添加观察者
当我们执行异步提取请求时,我们将立即获得一个NSAsynchronousFetchResult
对象。 该对象具有一个progress
属性,其类型为NSProgress
。 如果要接收进度更新,则需要观察此progress
属性。
// Create Progress
let progress = NSProgress(totalUnitCount: 1)
// Become Current
progress.becomeCurrentWithPendingUnitCount(1)
// Execute Asynchronous Fetch Request
let asynchronousFetchResult = try managedObjectContext.executeRequest(asynchronousFetchRequest) as! NSAsynchronousFetchResult
if let asynchronousFetchProgress = asynchronousFetchResult.progress {
asynchronousFetchProgress.addObserver(self, forKeyPath: "completedUnitCount", options: NSKeyValueObservingOptions.New, context: nil)
}
// Resign Current
progress.resignCurrent()
请注意,我们在progress
对象上调用了resignCurrent
,以平衡更早的becomeCurrentWithPendingUnitCount:
调用。 请记住,这两个方法都需要在同一线程上调用。
步骤4:移除观察者
在异步获取请求的完成块中,我们删除观察者并关闭进度HUD。
// Initialize Asynchronous Fetch Request
let asynchronousFetchRequest = NSAsynchronousFetchRequest(fetchRequest: fetchRequest) { (asynchronousFetchResult) -> Void in
dispatch_async(dispatch_get_main_queue(), { () -> Void in
// Dismiss Progress HUD
SVProgressHUD.dismiss()
// Process Asynchronous Fetch Result
self.processAsynchronousFetchResult(asynchronousFetchResult)
if let asynchronousFetchProgress = asynchronousFetchResult.progress {
// Remove Observer
asynchronousFetchProgress.removeObserver(self, forKeyPath: "completedUnitCount")
}
})
}
在实现observeValueForKeyPath(_:ofObject:change:context:)
,我们需要在创建异步获取请求之前显示进度HUD。
// Show Progress HUD
SVProgressHUD.showWithStatus("Fetching Data", maskType: .Gradient)
步骤5:进度报告
剩下要做的就是实现observeValueForKeyPath(_:ofObject:change:context:)
方法。 我们检查context
是否等于ProgressContext
,通过从change
字典中提取完成的记录数来创建status
对象,并更新进度HUD。 请注意,我们在主线程上更新了用户界面。
override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {
if keyPath == "completedUnitCount" {
dispatch_async(dispatch_get_main_queue(), { () -> Void in
if let changes = change, number = changes["new"] {
// Create Status
let status = "Fetched \(number) Records"
// Show Progress HUD
SVProgressHUD.setStatus(status)
}
})
}
}
5.虚拟数据
如果我们想正确地测试我们的应用程序,我们需要更多的数据。 尽管我不建议在生产应用程序中使用以下方法,但这是一种用数据填充数据库的快速简便的方法。
打开AppDelegate.swift并更新application(_:didFinishLaunchingWithOptions:)
方法,如下所示。 populateDatabase()
方法是一种简单的帮助程序方法,其中我们将虚拟数据添加到数据库中。
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
// Populate Database
populateDatabase()
...
return true
}
实现很简单。 因为我们只想插入一次伪数据,所以我们在用户默认数据库中检查键"didPopulateDatabase"
。 如果未设置密钥,我们将插入伪数据。
private func populateDatabase() {
// Helpers
let userDefaults = NSUserDefaults.standardUserDefaults()
guard userDefaults.objectForKey("didPopulateDatabase") == nil else { return }
// Create Entity
let entityDescription = NSEntityDescription.entityForName("Item", inManagedObjectContext: self.managedObjectContext)
for index in 0...1000000 {
// Initialize Record
let record = NSManagedObject(entity: entityDescription!, insertIntoManagedObjectContext: self.managedObjectContext)
// Populate Record
record.setValue(NSDate(), forKey: "createdAt")
record.setValue("Item \(index)", forKey: "name")
}
// Save Changes
saveManagedObjectContext()
// Update User Defaults
userDefaults.setBool(true, forKey: "didPopulateDatabase")
}
记录数很重要。 如果您打算在iOS Simulator上运行该应用程序,则可以插入100,000或1,000,000条记录。 这在物理设备上效果不佳,将花费很长时间才能完成。
在for
循环中,我们创建一个托管对象并向其中填充数据。 请注意,在for
循环的每次迭代期间,我们不会保存托管对象上下文的更改。
最后,我们更新用户默认数据库,以确保下次启动应用程序时不会填充该数据库。
大。 在iOS模拟器中运行应用程序以查看结果。 您会注意到,异步获取请求需要一些时间才能开始获取记录并更新进度HUD。
![显示异步获取请求的进度](https://i-blog.csdnimg.cn/blog_migrate/41bac81fe0187cea448b47185e269894.png)
6.重大变化
通过用异步获取请求替换获取的结果控制器类,我们破坏了应用程序的几部分。 例如,点按待办事项的复选标记似乎不再起作用。 在更新数据库时,用户界面不会反映出更改。 该解决方案很容易修复,我将由您自己来实施解决方案。 您现在应该拥有足够的知识来理解问题并找到合适的解决方案。
结论
我确信您同意异步获取非常容易使用。 繁重的工作由Core Data完成,这意味着无需手动将异步获取请求的结果与托管对象上下文合并。 当异步获取请求将结果交给您时,您唯一的工作就是更新用户界面。
翻译自: https://code.tutsplus.com/tutorials/core-data-and-swift-asynchronous-fetching--cms-25123