From 5461bb0736bc9cb12a1880155bdfec34d00e2a36 Mon Sep 17 00:00:00 2001 From: John Estropia Date: Thu, 10 Sep 2015 16:57:35 +0900 Subject: [PATCH] workaround an NSFetchedResultsController bug in Xcode 7 targeted on iOS 8 devices where errant index paths cause crashes --- CoreStore.xcodeproj/project.pbxproj | 4 + .../FetchedResultsControllerDelegate.swift | 175 ++++++++++++++++++ CoreStore/Observing/ListMonitor.swift | 78 +------- CoreStore/Observing/ObjectMonitor.swift | 69 ++----- 4 files changed, 200 insertions(+), 126 deletions(-) create mode 100644 CoreStore/Internal/FetchedResultsControllerDelegate.swift diff --git a/CoreStore.xcodeproj/project.pbxproj b/CoreStore.xcodeproj/project.pbxproj index 8086350..3bf61d7 100644 --- a/CoreStore.xcodeproj/project.pbxproj +++ b/CoreStore.xcodeproj/project.pbxproj @@ -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 = ""; }; B504D0D51B02362500B2BBB1 /* CoreStore+Setup.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CoreStore+Setup.swift"; sourceTree = ""; }; B51BE0691B47FC4B0069F532 /* NSManagedObjectModel+Setup.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSManagedObjectModel+Setup.swift"; sourceTree = ""; }; + B54A6A541BA15F2A007870FD /* FetchedResultsControllerDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FetchedResultsControllerDelegate.swift; sourceTree = ""; }; B56007101B3F6BD500A9A8F9 /* Into.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Into.swift; sourceTree = ""; }; B56007131B3F6C2800A9A8F9 /* SectionBy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SectionBy.swift; sourceTree = ""; }; B56007151B4018AB00A9A8F9 /* MigrationChain.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MigrationChain.swift; sourceTree = ""; }; @@ -434,6 +436,7 @@ B51BE0691B47FC4B0069F532 /* NSManagedObjectModel+Setup.swift */, B5E84F2D1AFF849C0064E85B /* WeakObject.swift */, B5E834BA1B7691F3001D3D50 /* Functions.swift */, + B54A6A541BA15F2A007870FD /* FetchedResultsControllerDelegate.swift */, ); path = Internal; sourceTree = ""; @@ -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 */, diff --git a/CoreStore/Internal/FetchedResultsControllerDelegate.swift b/CoreStore/Internal/FetchedResultsControllerDelegate.swift new file mode 100644 index 0000000..4c5f7b9 --- /dev/null +++ b/CoreStore/Internal/FetchedResultsControllerDelegate.swift @@ -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() + private var insertedSections = Set() +} diff --git a/CoreStore/Observing/ListMonitor.swift b/CoreStore/Observing/ListMonitor.swift index d0ed98f..b6d7a13 100644 --- a/CoreStore/Observing/ListMonitor.swift +++ b/CoreStore/Observing/ListMonitor.swift @@ -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" diff --git a/CoreStore/Observing/ObjectMonitor.swift b/CoreStore/Observing/ObjectMonitor.swift index 49376db..abef246 100644 --- a/CoreStore/Observing/ObjectMonitor.swift +++ b/CoreStore/Observing/ObjectMonitor.swift @@ -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 } }