当涉及部分操作时,不调用 performBatchUpdates 完成处理程序
performBatchUpdates completion handler is not called when there is section operation involved
到目前为止,根据提供的信息
,以下是几乎适用于 NSFetchedResultsController
+ UICollectionView
的代码片段
请注意,有 2 个 [BlockOperation]
,因为 reloadItems
和 moveItem
在单个 performBatchUpdates
中表现不佳。根据视频中提出的解决方法,我们必须在单独的 performBatchUpdates
.
中调用 reloadItems
我们也不遵循视频中提出的 100% 方法(首先执行 reloadItems
类型的 performBatchUpdates,然后插入/移动/删除类型的 performBatchUpdates)。
这是因为我们注意到即使对于简单的情况它也不能很好地工作。 一些奇怪的行为,包括 reloadItems
会导致重复的单元格 UI 显示在屏幕上 。我们找到的“几乎”工作方法是
- 为插入、移动和删除执行 performBatchUpdates
- 在 performBatchUpdates 的完成处理程序中,为 reloadItems 执行另一个 performBatchUpdates
NSFetchedResultsController + UICollectionView 集成
private var blockOperations: [BlockOperation] = []
// reloadItems and moveItem do not play well together. We are using the following workaround proposed at
// https://developer.apple.com/videos/play/wwdc2018/225/
private var blockUpdateOperations: [BlockOperation] = []
extension DashboardViewController: NSFetchedResultsControllerDelegate {
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
if type == NSFetchedResultsChangeType.insert {
print(">> insert")
blockOperations.append(
BlockOperation(block: { [weak self] in
if let self = self {
self.collectionView!.insertItems(at: [newIndexPath!])
}
})
)
}
else if type == NSFetchedResultsChangeType.update {
print(">> update")
blockUpdateOperations.append(
BlockOperation(block: { [weak self] in
if let self = self, let indexPath = indexPath {
self.collectionView.reloadItems(at: [indexPath])
}
})
)
}
else if type == NSFetchedResultsChangeType.move {
print(">> move")
blockOperations.append(
BlockOperation(block: { [weak self] in
if let self = self, let newIndexPath = newIndexPath, let indexPath = indexPath {
self.collectionView.moveItem(at: indexPath, to: newIndexPath)
}
})
)
}
else if type == NSFetchedResultsChangeType.delete {
print(">> delete")
blockOperations.append(
BlockOperation(block: { [weak self] in
if let self = self {
self.collectionView!.deleteItems(at: [indexPath!])
}
})
)
}
}
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange sectionInfo: NSFetchedResultsSectionInfo, atSectionIndex sectionIndex: Int, for type: NSFetchedResultsChangeType) {
if type == NSFetchedResultsChangeType.insert {
print(">> section insert")
blockOperations.append(
BlockOperation(block: { [weak self] in
if let self = self {
self.collectionView!.insertSections(IndexSet(integer: sectionIndex))
}
})
)
}
else if type == NSFetchedResultsChangeType.update {
print(">> section update")
blockOperations.append(
BlockOperation(block: { [weak self] in
if let self = self {
self.collectionView!.reloadSections(IndexSet(integer: sectionIndex))
}
})
)
}
else if type == NSFetchedResultsChangeType.delete {
print(">> section delete")
blockOperations.append(
BlockOperation(block: { [weak self] in
if let self = self {
self.collectionView!.deleteSections(IndexSet(integer: sectionIndex))
}
})
)
}
}
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
if blockOperations.isEmpty {
performBatchUpdatesForUpdateOperations()
} else {
collectionView.performBatchUpdates({ [weak self] () -> Void in
guard let self = self else { return }
for operation: BlockOperation in self.blockOperations {
operation.start()
}
self.blockOperations.removeAll(keepingCapacity: false)
}, completion: { [weak self] (finished) -> Void in
print("blockOperations completed")
guard let self = self else { return }
self.performBatchUpdatesForUpdateOperations()
})
}
}
private func performBatchUpdatesForUpdateOperations() {
if blockUpdateOperations.isEmpty {
return
}
collectionView.performBatchUpdates({ [weak self] () -> Void in
guard let self = self else { return }
for operation: BlockOperation in self.blockUpdateOperations {
operation.start()
}
self.blockUpdateOperations.removeAll(keepingCapacity: false)
}, completion: { [weak self] (finished) -> Void in
print("blockUpdateOperations completed")
guard let self = self else { return }
})
}
}
上述方法在不涉及“部分”操作时“几乎”运行良好。
对于上面的动画,你会观察到日志记录
>> move
blockOperations completed
>> move
blockOperations completed
>> move
blockOperations completed
但是,当添加/删除一个部分时,performBatchUpdates
的完成处理程序没有被调用!
对于上面的动画,你会观察到日志记录
>> section delete
>> move
>> section insert
>> move
这意味着完成处理块没有被执行!有谁知道为什么会这样,我该如何解决这个问题?
我希望打印出“blockOperations completed”。预期的日志应该是
>> section delete
>> move
blockOperations completed
>> section insert
>> move
blockOperations completed
谢谢。
我在 Xcode 12 和 Xcode 13.0 beta 上测试了这个。
在 Xcode 12 我可以重现您描述的错误:
更改对象以删除整个部分时,不会调用完成处理程序。在执行另一个后续更改时,我收到 两次 完成处理程序调用。
在 Xcode 13 然而,这个问题在我的测试中不能重现。当一个部分被清除和删除时,我会得到适当的回调。
尽管如此,我仍然在控制台中收到一条奇怪的消息说
[Snapshotting] Snapshotting a view (xxx, WhosebugDemo.Cell) that has not been rendered at least once requires afterScreenUpdates:YES.
我现在的结论是,这是系统中的一个错误,已在 iOS 15.
中修复
[更新]
尽管我已经更新了您的代码以在两个 os 版本上实现正确的行为。
关键概念是:
- 首先执行单值更新
- 第二次执行部分更新
- 在移动的情况下也会在完成块中执行重新加载,
否则 possible 同步更新将不会呈现
如果您存储移动的 indexPaths 并且只重新加载这些行,则可能 os 可以优化最后一步。
这是我为重现问题而添加的代码。
如果您想测试,请执行以下步骤:
- 创建一个新的 Xcode 项目
- 删除ViewController、SceneDelegate、Storyboard
- 从 info.plist
中删除故事板和场景引用
- 用下面的代码替换 AppDelegate 的内容(只是最小的样板 view/data 设置和委托方法)
import UIKit
import CoreData
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
self.window = UIWindow(frame: UIScreen.main.bounds)
let layout = UICollectionViewFlowLayout()
layout.headerReferenceSize = CGSize(width: 30,height: 30)
layout.sectionInset = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
self.window?.rootViewController = UINavigationController.init(rootViewController: DashboardViewController(collectionViewLayout: layout) )
self.window?.makeKeyAndVisible()
return true
}
}
class DashboardViewController: UICollectionViewController {
let persistentContainer = PersistentContainer()
lazy var resultsController: NSFetchedResultsController<Entity>? = {
let fetchRequest = NSFetchRequest<Entity>(entityName: "Entity")
fetchRequest.sortDescriptors = [NSSortDescriptor(key: "section", ascending: true), NSSortDescriptor(key: "name", ascending: false)]
let resultsController = NSFetchedResultsController(fetchRequest: fetchRequest,
managedObjectContext: self.persistentContainer.viewContext,
sectionNameKeyPath: "section",
cacheName: nil)
resultsController.delegate = self
try! resultsController.performFetch()
return resultsController
}()
private var itemOperations = [() -> Void]()
private var sectionOperations = [() -> Void]()
private var reloadRequired = false
override func viewDidLoad() {
super.viewDidLoad()
self.navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(add))
self.collectionView.register(Cell.self, forCellWithReuseIdentifier: "Cell")
self.collectionView.register(Header.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "Header")
}
var itemIndex: Int = 0
var section: Double = 0
@objc func add() {
let entity = Entity(context: self.persistentContainer.viewContext)
entity.name = Int64(self.itemIndex)
itemIndex += 1
entity.section = Int64(floor(self.section))
section += 0.5
try! self.persistentContainer.viewContext.save()
}
override func numberOfSections(in collectionView: UICollectionView) -> Int { return resultsController!.sections?.count ?? 0 }
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return self.resultsController!.sections![section].numberOfObjects }
override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
let header = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "Header", for: indexPath) as! Header
let sectionInfo = self.resultsController!.sections?[indexPath.section]
header.label.text = sectionInfo?.name
return header
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let item = self.resultsController?.object(at: indexPath)
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! Cell
cell.label.text = String(describing: item?.name ?? -1)
return cell
}
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let item = self.resultsController?.object(at: indexPath)
item?.section = max(0, (item?.section ?? 0) - 1)
item?.name = 10 + (item?.name ?? 0)
}
}
@objc(Entity)
public class Entity: NSManagedObject {
@NSManaged public var name: Int64
@NSManaged public var section: Int64
}
class Cell: UICollectionViewCell {
let label = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = .lightGray
self.label.textAlignment = .center
self.label.autoresizingMask = [.flexibleWidth, .flexibleHeight]
self.label.frame = self.contentView.bounds
self.label.translatesAutoresizingMaskIntoConstraints = true
self.contentView.addSubview(self.label)
}
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
}
class Header: UICollectionReusableView {
let label = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = .gray
self.label.textAlignment = .center
self.label.autoresizingMask = [.flexibleWidth, .flexibleHeight]
self.label.frame = self.bounds
self.label.translatesAutoresizingMaskIntoConstraints = true
self.addSubview(self.label)
}
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
}
class PersistentContainer: NSPersistentContainer {
convenience init() {
// create object model
let nameProperty = NSAttributeDescription()
nameProperty.name = "name"
nameProperty.attributeType = .integer64AttributeType
let sectionProperty = NSAttributeDescription()
sectionProperty.name = "section"
sectionProperty.attributeType = .integer64AttributeType
let entity = NSEntityDescription()
entity.name = "Entity"
entity.managedObjectClassName = "Entity"
entity.properties = [nameProperty, sectionProperty]
let model = NSManagedObjectModel()
model.entities.append(entity)
// create container
self.init(name: "Foo", managedObjectModel: model)
let description = NSPersistentStoreDescription()
description.type = NSInMemoryStoreType
self.persistentStoreDescriptions = [description]
self.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
}
}
extension DashboardViewController: NSFetchedResultsControllerDelegate {
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
reloadRequired = false
if type == NSFetchedResultsChangeType.insert {
print(">> insert")
itemOperations.append { [weak self] in
if let self = self {
self.collectionView!.insertItems(at: [newIndexPath!])
}
}
}
else if type == NSFetchedResultsChangeType.update {
print(">> update")
itemOperations.append { [weak self] in
if let self = self, let indexPath = indexPath {
self.collectionView.reloadItems(at: [indexPath])
}
}
}
else if type == NSFetchedResultsChangeType.move {
print(">> move")
self.reloadRequired = true
itemOperations.append { [weak self] in
if let self = self, let newIndexPath = newIndexPath, let indexPath = indexPath {
self.collectionView.moveItem(at: indexPath, to: newIndexPath)
}
}
}
else if type == NSFetchedResultsChangeType.delete {
print(">> delete")
itemOperations.append { [weak self] in
if let self = self {
self.collectionView!.deleteItems(at: [indexPath!])
}
}
}
}
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange sectionInfo: NSFetchedResultsSectionInfo, atSectionIndex sectionIndex: Int, for type: NSFetchedResultsChangeType) {
if type == NSFetchedResultsChangeType.insert {
print(">> section insert")
sectionOperations.append { [weak self] in
if let self = self {
self.collectionView!.insertSections(IndexSet(integer: sectionIndex))
}
}
}
else if type == NSFetchedResultsChangeType.update {
print(">> section update")
sectionOperations.append { [weak self] in
if let self = self {
self.collectionView!.reloadSections(IndexSet(integer: sectionIndex))
}
}
}
else if type == NSFetchedResultsChangeType.delete {
print(">> section delete")
sectionOperations.append { [weak self] in
if let self = self {
self.collectionView!.deleteSections(IndexSet(integer: sectionIndex))
}
}
}
}
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
collectionView.performBatchUpdates({ [weak self] () -> Void in
guard let self = self else { return }
// execute single item operations first
self.itemOperations.forEach { [=10=]() }
// execute section operations afterwards
self.sectionOperations.forEach { [=10=]() }
self.itemOperations.removeAll(keepingCapacity: false)
self.sectionOperations.removeAll(keepingCapacity: false)
}, completion: { [weak self] (finished) -> Void in
print("blockOperations completed")
guard let self = self else { return }
// in case of a move do a reload in case the item has also changed
// it will not update otherwise
if self.reloadRequired {
self.collectionView.reloadData()
}
})
}
}
到目前为止,根据提供的信息
,以下是几乎适用于NSFetchedResultsController
+ UICollectionView
的代码片段
请注意,有 2 个 [BlockOperation]
,因为 reloadItems
和 moveItem
在单个 performBatchUpdates
中表现不佳。根据视频中提出的解决方法,我们必须在单独的 performBatchUpdates
.
reloadItems
我们也不遵循视频中提出的 100% 方法(首先执行 reloadItems
类型的 performBatchUpdates,然后插入/移动/删除类型的 performBatchUpdates)。
这是因为我们注意到即使对于简单的情况它也不能很好地工作。 一些奇怪的行为,包括 reloadItems
会导致重复的单元格 UI 显示在屏幕上 。我们找到的“几乎”工作方法是
- 为插入、移动和删除执行 performBatchUpdates
- 在 performBatchUpdates 的完成处理程序中,为 reloadItems 执行另一个 performBatchUpdates
NSFetchedResultsController + UICollectionView 集成
private var blockOperations: [BlockOperation] = []
// reloadItems and moveItem do not play well together. We are using the following workaround proposed at
// https://developer.apple.com/videos/play/wwdc2018/225/
private var blockUpdateOperations: [BlockOperation] = []
extension DashboardViewController: NSFetchedResultsControllerDelegate {
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
if type == NSFetchedResultsChangeType.insert {
print(">> insert")
blockOperations.append(
BlockOperation(block: { [weak self] in
if let self = self {
self.collectionView!.insertItems(at: [newIndexPath!])
}
})
)
}
else if type == NSFetchedResultsChangeType.update {
print(">> update")
blockUpdateOperations.append(
BlockOperation(block: { [weak self] in
if let self = self, let indexPath = indexPath {
self.collectionView.reloadItems(at: [indexPath])
}
})
)
}
else if type == NSFetchedResultsChangeType.move {
print(">> move")
blockOperations.append(
BlockOperation(block: { [weak self] in
if let self = self, let newIndexPath = newIndexPath, let indexPath = indexPath {
self.collectionView.moveItem(at: indexPath, to: newIndexPath)
}
})
)
}
else if type == NSFetchedResultsChangeType.delete {
print(">> delete")
blockOperations.append(
BlockOperation(block: { [weak self] in
if let self = self {
self.collectionView!.deleteItems(at: [indexPath!])
}
})
)
}
}
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange sectionInfo: NSFetchedResultsSectionInfo, atSectionIndex sectionIndex: Int, for type: NSFetchedResultsChangeType) {
if type == NSFetchedResultsChangeType.insert {
print(">> section insert")
blockOperations.append(
BlockOperation(block: { [weak self] in
if let self = self {
self.collectionView!.insertSections(IndexSet(integer: sectionIndex))
}
})
)
}
else if type == NSFetchedResultsChangeType.update {
print(">> section update")
blockOperations.append(
BlockOperation(block: { [weak self] in
if let self = self {
self.collectionView!.reloadSections(IndexSet(integer: sectionIndex))
}
})
)
}
else if type == NSFetchedResultsChangeType.delete {
print(">> section delete")
blockOperations.append(
BlockOperation(block: { [weak self] in
if let self = self {
self.collectionView!.deleteSections(IndexSet(integer: sectionIndex))
}
})
)
}
}
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
if blockOperations.isEmpty {
performBatchUpdatesForUpdateOperations()
} else {
collectionView.performBatchUpdates({ [weak self] () -> Void in
guard let self = self else { return }
for operation: BlockOperation in self.blockOperations {
operation.start()
}
self.blockOperations.removeAll(keepingCapacity: false)
}, completion: { [weak self] (finished) -> Void in
print("blockOperations completed")
guard let self = self else { return }
self.performBatchUpdatesForUpdateOperations()
})
}
}
private func performBatchUpdatesForUpdateOperations() {
if blockUpdateOperations.isEmpty {
return
}
collectionView.performBatchUpdates({ [weak self] () -> Void in
guard let self = self else { return }
for operation: BlockOperation in self.blockUpdateOperations {
operation.start()
}
self.blockUpdateOperations.removeAll(keepingCapacity: false)
}, completion: { [weak self] (finished) -> Void in
print("blockUpdateOperations completed")
guard let self = self else { return }
})
}
}
上述方法在不涉及“部分”操作时“几乎”运行良好。
对于上面的动画,你会观察到日志记录
>> move
blockOperations completed
>> move
blockOperations completed
>> move
blockOperations completed
但是,当添加/删除一个部分时,performBatchUpdates
的完成处理程序没有被调用!
对于上面的动画,你会观察到日志记录
>> section delete
>> move
>> section insert
>> move
这意味着完成处理块没有被执行!有谁知道为什么会这样,我该如何解决这个问题?
我希望打印出“blockOperations completed”。预期的日志应该是
>> section delete
>> move
blockOperations completed
>> section insert
>> move
blockOperations completed
谢谢。
我在 Xcode 12 和 Xcode 13.0 beta 上测试了这个。
在 Xcode 12 我可以重现您描述的错误:
更改对象以删除整个部分时,不会调用完成处理程序。在执行另一个后续更改时,我收到 两次 完成处理程序调用。
在 Xcode 13 然而,这个问题在我的测试中不能重现。当一个部分被清除和删除时,我会得到适当的回调。
尽管如此,我仍然在控制台中收到一条奇怪的消息说
[Snapshotting] Snapshotting a view (xxx, WhosebugDemo.Cell) that has not been rendered at least once requires afterScreenUpdates:YES.
我现在的结论是,这是系统中的一个错误,已在 iOS 15.
中修复[更新]
尽管我已经更新了您的代码以在两个 os 版本上实现正确的行为。
关键概念是:
- 首先执行单值更新
- 第二次执行部分更新
- 在移动的情况下也会在完成块中执行重新加载, 否则 possible 同步更新将不会呈现
如果您存储移动的 indexPaths 并且只重新加载这些行,则可能 os 可以优化最后一步。
这是我为重现问题而添加的代码。
如果您想测试,请执行以下步骤:
- 创建一个新的 Xcode 项目
- 删除ViewController、SceneDelegate、Storyboard
- 从 info.plist 中删除故事板和场景引用
- 用下面的代码替换 AppDelegate 的内容(只是最小的样板 view/data 设置和委托方法)
import UIKit
import CoreData
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
self.window = UIWindow(frame: UIScreen.main.bounds)
let layout = UICollectionViewFlowLayout()
layout.headerReferenceSize = CGSize(width: 30,height: 30)
layout.sectionInset = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
self.window?.rootViewController = UINavigationController.init(rootViewController: DashboardViewController(collectionViewLayout: layout) )
self.window?.makeKeyAndVisible()
return true
}
}
class DashboardViewController: UICollectionViewController {
let persistentContainer = PersistentContainer()
lazy var resultsController: NSFetchedResultsController<Entity>? = {
let fetchRequest = NSFetchRequest<Entity>(entityName: "Entity")
fetchRequest.sortDescriptors = [NSSortDescriptor(key: "section", ascending: true), NSSortDescriptor(key: "name", ascending: false)]
let resultsController = NSFetchedResultsController(fetchRequest: fetchRequest,
managedObjectContext: self.persistentContainer.viewContext,
sectionNameKeyPath: "section",
cacheName: nil)
resultsController.delegate = self
try! resultsController.performFetch()
return resultsController
}()
private var itemOperations = [() -> Void]()
private var sectionOperations = [() -> Void]()
private var reloadRequired = false
override func viewDidLoad() {
super.viewDidLoad()
self.navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(add))
self.collectionView.register(Cell.self, forCellWithReuseIdentifier: "Cell")
self.collectionView.register(Header.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "Header")
}
var itemIndex: Int = 0
var section: Double = 0
@objc func add() {
let entity = Entity(context: self.persistentContainer.viewContext)
entity.name = Int64(self.itemIndex)
itemIndex += 1
entity.section = Int64(floor(self.section))
section += 0.5
try! self.persistentContainer.viewContext.save()
}
override func numberOfSections(in collectionView: UICollectionView) -> Int { return resultsController!.sections?.count ?? 0 }
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return self.resultsController!.sections![section].numberOfObjects }
override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
let header = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "Header", for: indexPath) as! Header
let sectionInfo = self.resultsController!.sections?[indexPath.section]
header.label.text = sectionInfo?.name
return header
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let item = self.resultsController?.object(at: indexPath)
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! Cell
cell.label.text = String(describing: item?.name ?? -1)
return cell
}
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let item = self.resultsController?.object(at: indexPath)
item?.section = max(0, (item?.section ?? 0) - 1)
item?.name = 10 + (item?.name ?? 0)
}
}
@objc(Entity)
public class Entity: NSManagedObject {
@NSManaged public var name: Int64
@NSManaged public var section: Int64
}
class Cell: UICollectionViewCell {
let label = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = .lightGray
self.label.textAlignment = .center
self.label.autoresizingMask = [.flexibleWidth, .flexibleHeight]
self.label.frame = self.contentView.bounds
self.label.translatesAutoresizingMaskIntoConstraints = true
self.contentView.addSubview(self.label)
}
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
}
class Header: UICollectionReusableView {
let label = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = .gray
self.label.textAlignment = .center
self.label.autoresizingMask = [.flexibleWidth, .flexibleHeight]
self.label.frame = self.bounds
self.label.translatesAutoresizingMaskIntoConstraints = true
self.addSubview(self.label)
}
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
}
class PersistentContainer: NSPersistentContainer {
convenience init() {
// create object model
let nameProperty = NSAttributeDescription()
nameProperty.name = "name"
nameProperty.attributeType = .integer64AttributeType
let sectionProperty = NSAttributeDescription()
sectionProperty.name = "section"
sectionProperty.attributeType = .integer64AttributeType
let entity = NSEntityDescription()
entity.name = "Entity"
entity.managedObjectClassName = "Entity"
entity.properties = [nameProperty, sectionProperty]
let model = NSManagedObjectModel()
model.entities.append(entity)
// create container
self.init(name: "Foo", managedObjectModel: model)
let description = NSPersistentStoreDescription()
description.type = NSInMemoryStoreType
self.persistentStoreDescriptions = [description]
self.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
}
}
extension DashboardViewController: NSFetchedResultsControllerDelegate {
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
reloadRequired = false
if type == NSFetchedResultsChangeType.insert {
print(">> insert")
itemOperations.append { [weak self] in
if let self = self {
self.collectionView!.insertItems(at: [newIndexPath!])
}
}
}
else if type == NSFetchedResultsChangeType.update {
print(">> update")
itemOperations.append { [weak self] in
if let self = self, let indexPath = indexPath {
self.collectionView.reloadItems(at: [indexPath])
}
}
}
else if type == NSFetchedResultsChangeType.move {
print(">> move")
self.reloadRequired = true
itemOperations.append { [weak self] in
if let self = self, let newIndexPath = newIndexPath, let indexPath = indexPath {
self.collectionView.moveItem(at: indexPath, to: newIndexPath)
}
}
}
else if type == NSFetchedResultsChangeType.delete {
print(">> delete")
itemOperations.append { [weak self] in
if let self = self {
self.collectionView!.deleteItems(at: [indexPath!])
}
}
}
}
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange sectionInfo: NSFetchedResultsSectionInfo, atSectionIndex sectionIndex: Int, for type: NSFetchedResultsChangeType) {
if type == NSFetchedResultsChangeType.insert {
print(">> section insert")
sectionOperations.append { [weak self] in
if let self = self {
self.collectionView!.insertSections(IndexSet(integer: sectionIndex))
}
}
}
else if type == NSFetchedResultsChangeType.update {
print(">> section update")
sectionOperations.append { [weak self] in
if let self = self {
self.collectionView!.reloadSections(IndexSet(integer: sectionIndex))
}
}
}
else if type == NSFetchedResultsChangeType.delete {
print(">> section delete")
sectionOperations.append { [weak self] in
if let self = self {
self.collectionView!.deleteSections(IndexSet(integer: sectionIndex))
}
}
}
}
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
collectionView.performBatchUpdates({ [weak self] () -> Void in
guard let self = self else { return }
// execute single item operations first
self.itemOperations.forEach { [=10=]() }
// execute section operations afterwards
self.sectionOperations.forEach { [=10=]() }
self.itemOperations.removeAll(keepingCapacity: false)
self.sectionOperations.removeAll(keepingCapacity: false)
}, completion: { [weak self] (finished) -> Void in
print("blockOperations completed")
guard let self = self else { return }
// in case of a move do a reload in case the item has also changed
// it will not update otherwise
if self.reloadRequired {
self.collectionView.reloadData()
}
})
}
}