workaround an NSFetchedResultsController bug in Xcode 7 targeted on iOS 8 devices where errant index paths cause crashes

This commit is contained in:
John Estropia
2015-09-10 16:57:35 +09:00
parent 4c6bc4f494
commit 5461bb0736
4 changed files with 200 additions and 126 deletions

View File

@@ -13,6 +13,7 @@
2F291E2719C6D3CF007AF63F /* CoreStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F291E2619C6D3CF007AF63F /* CoreStore.swift */; };
B504D0D61B02362500B2BBB1 /* CoreStore+Setup.swift in Sources */ = {isa = PBXBuildFile; fileRef = B504D0D51B02362500B2BBB1 /* CoreStore+Setup.swift */; };
B51BE06A1B47FC4B0069F532 /* NSManagedObjectModel+Setup.swift in Sources */ = {isa = PBXBuildFile; fileRef = B51BE0691B47FC4B0069F532 /* NSManagedObjectModel+Setup.swift */; };
B54A6A551BA15F2A007870FD /* FetchedResultsControllerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B54A6A541BA15F2A007870FD /* FetchedResultsControllerDelegate.swift */; settings = {ASSET_TAGS = (); }; };
B56007111B3F6BD500A9A8F9 /* Into.swift in Sources */ = {isa = PBXBuildFile; fileRef = B56007101B3F6BD500A9A8F9 /* Into.swift */; };
B56007141B3F6C2800A9A8F9 /* SectionBy.swift in Sources */ = {isa = PBXBuildFile; fileRef = B56007131B3F6C2800A9A8F9 /* SectionBy.swift */; };
B56007161B4018AB00A9A8F9 /* MigrationChain.swift in Sources */ = {isa = PBXBuildFile; fileRef = B56007151B4018AB00A9A8F9 /* MigrationChain.swift */; };
@@ -115,6 +116,7 @@
2F291E2619C6D3CF007AF63F /* CoreStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = CoreStore.swift; sourceTree = "<group>"; };
B504D0D51B02362500B2BBB1 /* CoreStore+Setup.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CoreStore+Setup.swift"; sourceTree = "<group>"; };
B51BE0691B47FC4B0069F532 /* NSManagedObjectModel+Setup.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSManagedObjectModel+Setup.swift"; sourceTree = "<group>"; };
B54A6A541BA15F2A007870FD /* FetchedResultsControllerDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FetchedResultsControllerDelegate.swift; sourceTree = "<group>"; };
B56007101B3F6BD500A9A8F9 /* Into.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Into.swift; sourceTree = "<group>"; };
B56007131B3F6C2800A9A8F9 /* SectionBy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SectionBy.swift; sourceTree = "<group>"; };
B56007151B4018AB00A9A8F9 /* MigrationChain.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MigrationChain.swift; sourceTree = "<group>"; };
@@ -434,6 +436,7 @@
B51BE0691B47FC4B0069F532 /* NSManagedObjectModel+Setup.swift */,
B5E84F2D1AFF849C0064E85B /* WeakObject.swift */,
B5E834BA1B7691F3001D3D50 /* Functions.swift */,
B54A6A541BA15F2A007870FD /* FetchedResultsControllerDelegate.swift */,
);
path = Internal;
sourceTree = "<group>";
@@ -576,6 +579,7 @@
B504D0D61B02362500B2BBB1 /* CoreStore+Setup.swift in Sources */,
B5D1E22C19FA9FBC003B2874 /* NSError+CoreStore.swift in Sources */,
B5E84F131AFF847B0064E85B /* Where.swift in Sources */,
B54A6A551BA15F2A007870FD /* FetchedResultsControllerDelegate.swift in Sources */,
B5A261211B64BFDB006EB6D3 /* MigrationType.swift in Sources */,
B5E84F141AFF847B0064E85B /* DataStack+Querying.swift in Sources */,
B56007141B3F6C2800A9A8F9 /* SectionBy.swift in Sources */,

View File

@@ -0,0 +1,175 @@
//
// FetchedResultsControllerDelegate.swift
// CoreStore
//
// Copyright (c) 2015 John Rommel Estropia
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
import Foundation
import CoreData
// MARK: - FetchedResultsControllerHandler
internal protocol FetchedResultsControllerHandler: class {
func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?)
func controller(controller: NSFetchedResultsController, didChangeSection sectionInfo: NSFetchedResultsSectionInfo, atIndex sectionIndex: Int, forChangeType type: NSFetchedResultsChangeType)
func controllerWillChangeContent(controller: NSFetchedResultsController)
func controllerDidChangeContent(controller: NSFetchedResultsController)
func controller(controller: NSFetchedResultsController, sectionIndexTitleForSectionName sectionName: String?) -> String?
}
// MARK: - FetchedResultsControllerDelegate
internal final class FetchedResultsControllerDelegate: NSObject, NSFetchedResultsControllerDelegate {
// MARK: Internal
internal weak var handler: FetchedResultsControllerHandler?
internal weak var fetchedResultsController: NSFetchedResultsController? {
didSet {
oldValue?.delegate = nil
self.fetchedResultsController?.delegate = self
}
}
deinit {
self.fetchedResultsController?.delegate = nil
}
// MARK: NSFetchedResultsControllerDelegate
@objc dynamic func controllerWillChangeContent(controller: NSFetchedResultsController) {
self.deletedSections = []
self.insertedSections = []
self.handler?.controllerWillChangeContent(controller)
}
@objc dynamic func controllerDidChangeContent(controller: NSFetchedResultsController) {
self.handler?.controllerDidChangeContent(controller)
}
@objc dynamic func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?) {
if #available(iOS 9, *) {
self.handler?.controller(
controller,
didChangeObject: anObject,
atIndexPath: indexPath,
forChangeType: type,
newIndexPath: newIndexPath
)
return
}
// Workaround 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
switch type {
case .Move:
guard let indexPath = indexPath, let newIndexPath = newIndexPath else {
return
}
if indexPath == newIndexPath
&& self.deletedSections.contains(indexPath.section) {
self.handler?.controller(
controller,
didChangeObject: anObject,
atIndexPath: nil,
forChangeType: .Insert,
newIndexPath: indexPath
)
return
}
case .Update:
guard let section = indexPath?.section else {
return
}
if self.deletedSections.contains(section)
|| self.insertedSections.contains(section) {
return
}
default:
break
}
self.handler?.controller(
controller,
didChangeObject: anObject,
atIndexPath: indexPath,
forChangeType: type,
newIndexPath: newIndexPath
)
}
@objc dynamic func controller(controller: NSFetchedResultsController, didChangeSection sectionInfo: NSFetchedResultsSectionInfo, atIndex sectionIndex: Int, forChangeType type: NSFetchedResultsChangeType) {
switch type {
case .Delete: self.deletedSections.insert(sectionIndex)
case .Insert: self.insertedSections.insert(sectionIndex)
default: break
}
self.handler?.controller(
controller,
didChangeSection: sectionInfo,
atIndex: sectionIndex,
forChangeType: type
)
}
@objc dynamic func controller(controller: NSFetchedResultsController, sectionIndexTitleForSectionName sectionName: String) -> String? {
return self.handler?.controller(
controller,
sectionIndexTitleForSectionName: sectionName
)
}
// MARK: Private
private var deletedSections = Set<Int>()
private var insertedSections = Set<Int>()
}

View File

@@ -955,7 +955,7 @@ extension ListMonitor: FetchedResultsControllerHandler {
// MARK: FetchedResultsControllerHandler
private func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?) {
internal func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?) {
switch type {
@@ -990,7 +990,6 @@ extension ListMonitor: FetchedResultsControllerHandler {
)
case .Move:
CoreStore.log(.Notice, message: "\(indexPath!) - \(newIndexPath!)")
NSNotificationCenter.defaultCenter().postNotificationName(
ListMonitorDidMoveObjectNotification,
object: self,
@@ -1003,7 +1002,7 @@ extension ListMonitor: FetchedResultsControllerHandler {
}
}
private func controller(controller: NSFetchedResultsController, didChangeSection sectionInfo: NSFetchedResultsSectionInfo, atIndex sectionIndex: Int, forChangeType type: NSFetchedResultsChangeType) {
internal func controller(controller: NSFetchedResultsController, didChangeSection sectionInfo: NSFetchedResultsSectionInfo, atIndex sectionIndex: Int, forChangeType type: NSFetchedResultsChangeType) {
switch type {
@@ -1032,7 +1031,7 @@ extension ListMonitor: FetchedResultsControllerHandler {
}
}
private func controllerWillChangeContent(controller: NSFetchedResultsController) {
internal func controllerWillChangeContent(controller: NSFetchedResultsController) {
self.taskGroup.enter()
NSNotificationCenter.defaultCenter().postNotificationName(
@@ -1041,7 +1040,7 @@ extension ListMonitor: FetchedResultsControllerHandler {
)
}
private func controllerDidChangeContent(controller: NSFetchedResultsController) {
internal func controllerDidChangeContent(controller: NSFetchedResultsController) {
NSNotificationCenter.defaultCenter().postNotificationName(
ListMonitorDidChangeListNotification,
@@ -1050,80 +1049,13 @@ extension ListMonitor: FetchedResultsControllerHandler {
self.taskGroup.leave()
}
private func controller(controller: NSFetchedResultsController, sectionIndexTitleForSectionName sectionName: String?) -> String? {
internal func controller(controller: NSFetchedResultsController, sectionIndexTitleForSectionName sectionName: String?) -> String? {
return self.sectionIndexTransformer(sectionName: sectionName)
}
}
// MARK: - FetchedResultsControllerHandler
private protocol FetchedResultsControllerHandler: class {
func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?)
func controller(controller: NSFetchedResultsController, didChangeSection sectionInfo: NSFetchedResultsSectionInfo, atIndex sectionIndex: Int, forChangeType type: NSFetchedResultsChangeType)
func controllerWillChangeContent(controller: NSFetchedResultsController)
func controllerDidChangeContent(controller: NSFetchedResultsController)
func controller(controller: NSFetchedResultsController, sectionIndexTitleForSectionName sectionName: String?) -> String?
}
// MARK: - FetchedResultsControllerDelegate
private final class FetchedResultsControllerDelegate: NSObject, NSFetchedResultsControllerDelegate {
// MARK: NSFetchedResultsControllerDelegate
@objc dynamic func controllerWillChangeContent(controller: NSFetchedResultsController) {
self.handler?.controllerWillChangeContent(controller)
}
@objc dynamic func controllerDidChangeContent(controller: NSFetchedResultsController) {
self.handler?.controllerDidChangeContent(controller)
}
@objc dynamic func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?) {
self.handler?.controller(controller, didChangeObject: anObject, atIndexPath: indexPath, forChangeType: type, newIndexPath: newIndexPath)
}
@objc dynamic func controller(controller: NSFetchedResultsController, didChangeSection sectionInfo: NSFetchedResultsSectionInfo, atIndex sectionIndex: Int, forChangeType type: NSFetchedResultsChangeType) {
self.handler?.controller(controller, didChangeSection: sectionInfo, atIndex: sectionIndex, forChangeType: type)
}
@objc dynamic func controller(controller: NSFetchedResultsController, sectionIndexTitleForSectionName sectionName: String) -> String? {
return self.handler?.controller(controller, sectionIndexTitleForSectionName: sectionName)
}
// MARK: Private
weak var handler: FetchedResultsControllerHandler?
weak var fetchedResultsController: NSFetchedResultsController? {
didSet {
oldValue?.delegate = nil
self.fetchedResultsController?.delegate = self
}
}
deinit {
self.fetchedResultsController?.delegate = nil
}
}
private let ListMonitorWillChangeListNotification = "ListMonitorWillChangeListNotification"
private let ListMonitorDidChangeListNotification = "ListMonitorDidChangeListNotification"
private let ListMonitorWillRefetchListNotification = "ListMonitorWillRefetchListNotification"

View File

@@ -268,7 +268,17 @@ extension ObjectMonitor: FetchedResultsControllerHandler {
// MARK: FetchedResultsControllerHandler
private func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?) {
internal func controllerWillChangeContent(controller: NSFetchedResultsController) {
NSNotificationCenter.defaultCenter().postNotificationName(
ObjectMonitorWillChangeObjectNotification,
object: self
)
}
internal func controllerDidChangeContent(controller: NSFetchedResultsController) { }
internal func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?) {
switch type {
@@ -279,7 +289,7 @@ extension ObjectMonitor: FetchedResultsControllerHandler {
userInfo: [UserInfoKeyObject: anObject]
)
case .Update, .Move where indexPath == newIndexPath:
case .Update:
NSNotificationCenter.defaultCenter().postNotificationName(
ObjectMonitorDidUpdateObjectNotification,
object: self,
@@ -291,58 +301,11 @@ extension ObjectMonitor: FetchedResultsControllerHandler {
}
}
private func controllerWillChangeContent(controller: NSFetchedResultsController) {
internal func controller(controller: NSFetchedResultsController, didChangeSection sectionInfo: NSFetchedResultsSectionInfo, atIndex sectionIndex: Int, forChangeType type: NSFetchedResultsChangeType) { }
internal func controller(controller: NSFetchedResultsController, sectionIndexTitleForSectionName sectionName: String?) -> String? {
NSNotificationCenter.defaultCenter().postNotificationName(
ObjectMonitorWillChangeObjectNotification,
object: self
)
}
}
// MARK: - FetchedResultsControllerHandler
private protocol FetchedResultsControllerHandler: class {
func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?)
func controllerWillChangeContent(controller: NSFetchedResultsController)
}
// MARK: - FetchedResultsControllerDelegate
private final class FetchedResultsControllerDelegate: NSObject, NSFetchedResultsControllerDelegate {
// MARK: NSFetchedResultsControllerDelegate
@objc dynamic func controllerWillChangeContent(controller: NSFetchedResultsController) {
self.handler?.controllerWillChangeContent(controller)
}
@objc dynamic func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?) {
self.handler?.controller(controller, didChangeObject: anObject, atIndexPath: indexPath, forChangeType: type, newIndexPath: newIndexPath)
}
// MARK: Private
weak var handler: FetchedResultsControllerHandler?
weak var fetchedResultsController: NSFetchedResultsController? {
didSet {
oldValue?.delegate = nil
self.fetchedResultsController?.delegate = self
}
}
deinit {
self.fetchedResultsController?.delegate = nil
return sectionName
}
}