diff --git a/Sources/FetchedResultsControllerDelegate.swift b/Sources/FetchedResultsControllerDelegate.swift index de4b073..6d29988 100644 --- a/Sources/FetchedResultsControllerDelegate.swift +++ b/Sources/FetchedResultsControllerDelegate.swift @@ -86,6 +86,14 @@ internal final class FetchedResultsControllerDelegate: NSObject, NSFetchedResult return } + + if #available(iOS 11.0, tvOS 11.0, watchOS 4.0, macOS 10.13, *) {} + else { + + self.deletedSections = [] + self.insertedSections = [] + } + self.handler?.controllerWillChangeContent(controller) } @@ -100,6 +108,7 @@ internal final class FetchedResultsControllerDelegate: NSObject, NSFetchedResult return } + self.handler?.controllerDidChangeContent(controller) } @@ -110,11 +119,102 @@ internal final class FetchedResultsControllerDelegate: NSObject, NSFetchedResult return } + + if #available(iOS 11.0, tvOS 11.0, watchOS 4.0, macOS 10.13, *) { + + self.handler?.controller( + controller, + didChangeObject: anObject, + atIndexPath: indexPath, + forChangeType: type, + newIndexPath: newIndexPath + ) + return + } + + guard var actualType = NSFetchedResultsChangeType(rawValue: type.rawValue) else { + + // This fix is for a bug where iOS passes 0 for NSFetchedResultsChangeType, but this is not a valid enum case. + // Swift will then always execute the first case of the switch causing strange behaviour. + // https://forums.developer.apple.com/thread/12184#31850 + return + } + + // This whole dance is a workaround for a nasty bug introduced in XCode 7 targeted at iOS 8 devices + // http://stackoverflow.com/questions/31383760/ios-9-attempt-to-delete-and-reload-the-same-index-path/31384014#31384014 + // https://forums.developer.apple.com/message/9998#9998 + // https://forums.developer.apple.com/message/31849#31849 + + if case .update = actualType, + indexPath != nil, + newIndexPath != nil { + + actualType = .move + } + + switch actualType { + + case .update: + guard let section = indexPath?[0] else { + + return + } + if self.deletedSections.contains(section) + || self.insertedSections.contains(section) { + + return + } + + case .move: + guard let indexPath = indexPath, let newIndexPath = newIndexPath else { + + return + } + guard indexPath == newIndexPath else { + + break + } + if self.insertedSections.contains(indexPath[0]) { + + // Observers that handle the .Move change are advised to delete then reinsert the object instead of just moving. This is especially true when indexPath and newIndexPath are equal. For example, calling tableView.moveRowAtIndexPath(_:toIndexPath) when both indexPaths are the same will crash the tableView. + self.handler?.controller( + controller, + didChangeObject: anObject, + atIndexPath: indexPath, + forChangeType: .move, + newIndexPath: newIndexPath + ) + return + } + if self.deletedSections.contains(indexPath[0]) { + + self.handler?.controller( + controller, + didChangeObject: anObject, + atIndexPath: nil, + forChangeType: .insert, + newIndexPath: indexPath + ) + return + } + self.handler?.controller( + controller, + didChangeObject: anObject, + atIndexPath: indexPath, + forChangeType: .update, + newIndexPath: nil + ) + return + + default: + break + } + self.handler?.controller( controller, didChangeObject: anObject, atIndexPath: indexPath, - forChangeType: type, + forChangeType: actualType, newIndexPath: newIndexPath ) } @@ -126,6 +226,18 @@ internal final class FetchedResultsControllerDelegate: NSObject, NSFetchedResult return } + + if #available(iOS 11.0, tvOS 11.0, watchOS 4.0, macOS 10.13, *) {} + else { + + switch type { + + case .delete: self.deletedSections.insert(sectionIndex) + case .insert: self.insertedSections.insert(sectionIndex) + default: break + } + } + self.handler?.controller( controller, didChangeSection: sectionInfo, @@ -142,4 +254,13 @@ internal final class FetchedResultsControllerDelegate: NSObject, NSFetchedResult sectionIndexTitleForSectionName: sectionName ) } + + + // MARK: Private + + @nonobjc + private var deletedSections = Set() + + @nonobjc + private var insertedSections = Set() }