├── .gitignore ├── README.md ├── SNRFetchedResultsController.podspec └── SNRFetchedResultsController ├── SNRFetchedResultsController.xcodeproj ├── project.pbxproj └── xcuserdata │ └── Coleman.xcuserdatad │ └── xcschemes │ ├── SNRFetchedResultsController.xcscheme │ └── xcschememanagement.plist └── SNRFetchedResultsController ├── Info.plist ├── SNRFetchedResultsController.h └── SNRFetchedResultsController.m /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.swp 3 | *~.nib 4 | 5 | build/ 6 | 7 | *.pbxuser 8 | *.perspective 9 | *.perspectivev3 10 | *.mode1v3 11 | *.mode2v3 12 | *.xcuserdata -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## SNRFetchedResultsController: Automatic Core Data change tracking for OS X 2 | 3 | `SNRFetchedResultsController` is a "port" (not exactly) of `NSFetchedResultsController` from iOS to OS X. It is not a drop in replacement for `NSFetchedResultsController`, but performs many of the same tasks in relation to managing results from a Core Data fetch and notifying a delegate when objects are inserted, deleted, updated, or moved in order to update the UI. 4 | 5 | **This project is in its early stages and probably has a few (many?) bugs. Any feedback, bug reports, and code contributions are greatly appreciated.** 6 | 7 | ## NSFetchedResultsController vs. SNRFetchedResultsController 8 | 9 | ### Limitations 10 | 11 | - `SNRFetchedResultsController` does not support sections or caching, mainly because (unlike `UITableView`) `NSTableView` does not support sections. That said, `SNRFetchedResultsController` can be used with custom UI controls that do support sectioning, so this would be a nice feature to add in the future. 12 | 13 | ### Differences 14 | 15 | - As a result of having no section support, `SNRFetchedResultsController` uses indexes (`NSUInteger`) instead of `NSIndexPath` 16 | 17 | ## ARC 18 | 19 | This project was written assuming that the code would be compiled under ARC. This means that there is no memory management code present, so if you are going to use this class in a non-ARC project then the correct retain/release calls will have to be inserted manually OR you will have to compile `SNRFetchedResultsController.m` with the `-fobjc-arc` flag. 20 | 21 | ## Example Usage 22 | 23 | ### Creating a fetched results controller 24 | 25 | ```` 26 | NSFetchRequest *request = [[NSFetchRequest alloc] init]; 27 | request.entity = [NSEntityDescription entityForName:@"Car" inManagedObjectContext:context]; 28 | request.sortDescriptors = [NSArray arrayWithObjects:[NSSortDescriptor sortDescriptorWithKey:@"year" ascending:YES], nil]; 29 | request.predicate = [NSPredicate predicateWithFormat:@"wheels.@count != 0"]; 30 | request.fetchBatchSize = 20; 31 | self.fetchedResultsController = [[SNRFetchedResultsController alloc] initWithManagedObjectContext:context fetchRequest:request]; 32 | self.fetchedResultsController.delegate = self; 33 | NSError *error = nil; 34 | [self.fetchedResultsController performFetch:&error]; 35 | if (error) { 36 | NSLog(@"Unresolved error: %@ %@", error, [error userInfo]); 37 | } 38 | ```` 39 | 40 | ### NSTableViewDataSource implementation 41 | 42 | ```` 43 | - (NSInteger)numberOfRowsInTableView:(NSTableView *)aTableView 44 | { 45 | return [self.fetchedResultsController count]; 46 | } 47 | - (id)tableView:(NSTableView *)aTableView objectValueForTableColumn:(NSTableColumn *)aTableColumn row:(NSInteger)rowIndex 48 | { 49 | return [self.fetchedResultsController objectAtIndex:rowIndex]; 50 | } 51 | ```` 52 | 53 | ### SNRFetchedResultsControllerDelegate implementation for NSTableView 54 | 55 | ```` 56 | - (void)controller:(SNRFetchedResultsController *)controller didChangeObject:(id)anObject atIndex:(NSUInteger)index forChangeType:(SNRFetchedResultsChangeType)type newIndex:(NSUInteger)newIndex 57 | { 58 | switch (type) { 59 | case SNRFetchedResultsChangeDelete: 60 | [self.tableView removeRowsAtIndexes:[NSIndexSet indexSetWithIndex:index] withAnimation:NSTableViewAnimationSlideLeft]; 61 | break; 62 | case SNRFetchedResultsChangeInsert: 63 | [self.tableView insertRowsAtIndexes:[NSIndexSet indexSetWithIndex:newIndex] withAnimation:NSTableViewAnimationSlideDown]; 64 | break; 65 | case SNRFetchedResultsChangeUpdate: 66 | [self.tableView reloadDataForRowIndexes:[NSIndexSet indexSetWithIndex:index] columnIndexes:[NSIndexSet indexSetWithIndex:0]]; 67 | break; 68 | case SNRFetchedResultsChangeMove: 69 | [self.tableView removeRowsAtIndexes:[NSIndexSet indexSetWithIndex:index] withAnimation:NSTableViewAnimationSlideLeft]; 70 | [self.tableView insertRowsAtIndexes:[NSIndexSet indexSetWithIndex:newIndex] withAnimation:NSTableViewAnimationSlideDown]; 71 | break; 72 | default: 73 | break; 74 | } 75 | } 76 | ```` 77 | 78 | ## Who am I? 79 | 80 | I'm Indragie Karunaratne, a 17 year old Mac OS X and iOS Developer from Edmonton AB, Canada. Visit [my website](http://indragie.com) to check out my work, or to get in touch with me. ([follow me](http://twitter.com/indragie) on Twitter!) 81 | 82 | ## Licensing 83 | 84 | `SNRFetchedResultsController` is licensed under the [BSD license](http://www.opensource.org/licenses/bsd-license.php). -------------------------------------------------------------------------------- /SNRFetchedResultsController.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |spec| 2 | spec.name = 'SNRFetchedResultsController' 3 | spec.version = '0.0.1' 4 | spec.license = { :type => 'BSD' } 5 | spec.homepage = 'https://github.com/indragiek/SNRFetchedResultsController' 6 | spec.authors = { 'Indragie Karunaratne' => 'i@indragie.com' } 7 | spec.summary = 'Automatic Core Data change tracking for OS X (NSFetchedResultsController port).' 8 | spec.source = { :git => 'https://github.com/indragiek/SNRFetchedResultsController.git', :tag => 'v0.0.1' } 9 | spec.source_files = 'SNRFetchedResultsController.{h,m}' 10 | spec.framework = 'SystemConfiguration' 11 | spec.platform = :osx 12 | spec.requires_arc = true 13 | end 14 | -------------------------------------------------------------------------------- /SNRFetchedResultsController/SNRFetchedResultsController.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 6EF11ECB1AF00AD700A7C4BC /* SNRFetchedResultsController.h in Headers */ = {isa = PBXBuildFile; fileRef = 6EF11ECA1AF00AD700A7C4BC /* SNRFetchedResultsController.h */; settings = {ATTRIBUTES = (Public, ); }; }; 11 | 6EF11EEB1AF00B3C00A7C4BC /* SNRFetchedResultsController.m in Sources */ = {isa = PBXBuildFile; fileRef = 6EF11EEA1AF00B3C00A7C4BC /* SNRFetchedResultsController.m */; }; 12 | /* End PBXBuildFile section */ 13 | 14 | /* Begin PBXFileReference section */ 15 | 6EF11EC51AF00AD700A7C4BC /* SNRFetchedResultsController.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SNRFetchedResultsController.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 16 | 6EF11EC91AF00AD700A7C4BC /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 17 | 6EF11ECA1AF00AD700A7C4BC /* SNRFetchedResultsController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SNRFetchedResultsController.h; sourceTree = ""; }; 18 | 6EF11EEA1AF00B3C00A7C4BC /* SNRFetchedResultsController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SNRFetchedResultsController.m; sourceTree = ""; }; 19 | /* End PBXFileReference section */ 20 | 21 | /* Begin PBXFrameworksBuildPhase section */ 22 | 6EF11EC11AF00AD700A7C4BC /* Frameworks */ = { 23 | isa = PBXFrameworksBuildPhase; 24 | buildActionMask = 2147483647; 25 | files = ( 26 | ); 27 | runOnlyForDeploymentPostprocessing = 0; 28 | }; 29 | /* End PBXFrameworksBuildPhase section */ 30 | 31 | /* Begin PBXGroup section */ 32 | 6EF11EBB1AF00AD700A7C4BC = { 33 | isa = PBXGroup; 34 | children = ( 35 | 6EF11EC71AF00AD700A7C4BC /* SNRFetchedResultsController */, 36 | 6EF11EC61AF00AD700A7C4BC /* Products */, 37 | ); 38 | sourceTree = ""; 39 | }; 40 | 6EF11EC61AF00AD700A7C4BC /* Products */ = { 41 | isa = PBXGroup; 42 | children = ( 43 | 6EF11EC51AF00AD700A7C4BC /* SNRFetchedResultsController.framework */, 44 | ); 45 | name = Products; 46 | sourceTree = ""; 47 | }; 48 | 6EF11EC71AF00AD700A7C4BC /* SNRFetchedResultsController */ = { 49 | isa = PBXGroup; 50 | children = ( 51 | 6EF11ECA1AF00AD700A7C4BC /* SNRFetchedResultsController.h */, 52 | 6EF11EEA1AF00B3C00A7C4BC /* SNRFetchedResultsController.m */, 53 | 6EF11EC81AF00AD700A7C4BC /* Supporting Files */, 54 | ); 55 | path = SNRFetchedResultsController; 56 | sourceTree = ""; 57 | }; 58 | 6EF11EC81AF00AD700A7C4BC /* Supporting Files */ = { 59 | isa = PBXGroup; 60 | children = ( 61 | 6EF11EC91AF00AD700A7C4BC /* Info.plist */, 62 | ); 63 | name = "Supporting Files"; 64 | sourceTree = ""; 65 | }; 66 | /* End PBXGroup section */ 67 | 68 | /* Begin PBXHeadersBuildPhase section */ 69 | 6EF11EC21AF00AD700A7C4BC /* Headers */ = { 70 | isa = PBXHeadersBuildPhase; 71 | buildActionMask = 2147483647; 72 | files = ( 73 | 6EF11ECB1AF00AD700A7C4BC /* SNRFetchedResultsController.h in Headers */, 74 | ); 75 | runOnlyForDeploymentPostprocessing = 0; 76 | }; 77 | /* End PBXHeadersBuildPhase section */ 78 | 79 | /* Begin PBXNativeTarget section */ 80 | 6EF11EC41AF00AD700A7C4BC /* SNRFetchedResultsController */ = { 81 | isa = PBXNativeTarget; 82 | buildConfigurationList = 6EF11EDB1AF00AD700A7C4BC /* Build configuration list for PBXNativeTarget "SNRFetchedResultsController" */; 83 | buildPhases = ( 84 | 6EF11EC01AF00AD700A7C4BC /* Sources */, 85 | 6EF11EC11AF00AD700A7C4BC /* Frameworks */, 86 | 6EF11EC21AF00AD700A7C4BC /* Headers */, 87 | 6EF11EC31AF00AD700A7C4BC /* Resources */, 88 | ); 89 | buildRules = ( 90 | ); 91 | dependencies = ( 92 | ); 93 | name = SNRFetchedResultsController; 94 | productName = SNRFetchedResultsController; 95 | productReference = 6EF11EC51AF00AD700A7C4BC /* SNRFetchedResultsController.framework */; 96 | productType = "com.apple.product-type.framework"; 97 | }; 98 | /* End PBXNativeTarget section */ 99 | 100 | /* Begin PBXProject section */ 101 | 6EF11EBC1AF00AD700A7C4BC /* Project object */ = { 102 | isa = PBXProject; 103 | attributes = { 104 | CLASSPREFIX = SNR; 105 | LastUpgradeCheck = 0630; 106 | ORGANIZATIONNAME = indragiek; 107 | TargetAttributes = { 108 | 6EF11EC41AF00AD700A7C4BC = { 109 | CreatedOnToolsVersion = 6.3; 110 | }; 111 | }; 112 | }; 113 | buildConfigurationList = 6EF11EBF1AF00AD700A7C4BC /* Build configuration list for PBXProject "SNRFetchedResultsController" */; 114 | compatibilityVersion = "Xcode 3.2"; 115 | developmentRegion = English; 116 | hasScannedForEncodings = 0; 117 | knownRegions = ( 118 | en, 119 | ); 120 | mainGroup = 6EF11EBB1AF00AD700A7C4BC; 121 | productRefGroup = 6EF11EC61AF00AD700A7C4BC /* Products */; 122 | projectDirPath = ""; 123 | projectRoot = ""; 124 | targets = ( 125 | 6EF11EC41AF00AD700A7C4BC /* SNRFetchedResultsController */, 126 | ); 127 | }; 128 | /* End PBXProject section */ 129 | 130 | /* Begin PBXResourcesBuildPhase section */ 131 | 6EF11EC31AF00AD700A7C4BC /* Resources */ = { 132 | isa = PBXResourcesBuildPhase; 133 | buildActionMask = 2147483647; 134 | files = ( 135 | ); 136 | runOnlyForDeploymentPostprocessing = 0; 137 | }; 138 | /* End PBXResourcesBuildPhase section */ 139 | 140 | /* Begin PBXSourcesBuildPhase section */ 141 | 6EF11EC01AF00AD700A7C4BC /* Sources */ = { 142 | isa = PBXSourcesBuildPhase; 143 | buildActionMask = 2147483647; 144 | files = ( 145 | 6EF11EEB1AF00B3C00A7C4BC /* SNRFetchedResultsController.m in Sources */, 146 | ); 147 | runOnlyForDeploymentPostprocessing = 0; 148 | }; 149 | /* End PBXSourcesBuildPhase section */ 150 | 151 | /* Begin XCBuildConfiguration section */ 152 | 6EF11ED91AF00AD700A7C4BC /* Debug */ = { 153 | isa = XCBuildConfiguration; 154 | buildSettings = { 155 | ALWAYS_SEARCH_USER_PATHS = NO; 156 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 157 | CLANG_CXX_LIBRARY = "libc++"; 158 | CLANG_ENABLE_MODULES = YES; 159 | CLANG_ENABLE_OBJC_ARC = YES; 160 | CLANG_WARN_BOOL_CONVERSION = YES; 161 | CLANG_WARN_CONSTANT_CONVERSION = YES; 162 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 163 | CLANG_WARN_EMPTY_BODY = YES; 164 | CLANG_WARN_ENUM_CONVERSION = YES; 165 | CLANG_WARN_INT_CONVERSION = YES; 166 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 167 | CLANG_WARN_UNREACHABLE_CODE = YES; 168 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 169 | COPY_PHASE_STRIP = NO; 170 | CURRENT_PROJECT_VERSION = 1; 171 | DEBUG_INFORMATION_FORMAT = dwarf; 172 | ENABLE_STRICT_OBJC_MSGSEND = YES; 173 | GCC_C_LANGUAGE_STANDARD = gnu99; 174 | GCC_DYNAMIC_NO_PIC = NO; 175 | GCC_NO_COMMON_BLOCKS = YES; 176 | GCC_OPTIMIZATION_LEVEL = 0; 177 | GCC_PREPROCESSOR_DEFINITIONS = ( 178 | "DEBUG=1", 179 | "$(inherited)", 180 | ); 181 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 182 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 183 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 184 | GCC_WARN_UNDECLARED_SELECTOR = YES; 185 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 186 | GCC_WARN_UNUSED_FUNCTION = YES; 187 | GCC_WARN_UNUSED_VARIABLE = YES; 188 | MACOSX_DEPLOYMENT_TARGET = 10.10; 189 | MTL_ENABLE_DEBUG_INFO = YES; 190 | ONLY_ACTIVE_ARCH = YES; 191 | SDKROOT = macosx; 192 | VERSIONING_SYSTEM = "apple-generic"; 193 | VERSION_INFO_PREFIX = ""; 194 | }; 195 | name = Debug; 196 | }; 197 | 6EF11EDA1AF00AD700A7C4BC /* Release */ = { 198 | isa = XCBuildConfiguration; 199 | buildSettings = { 200 | ALWAYS_SEARCH_USER_PATHS = NO; 201 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 202 | CLANG_CXX_LIBRARY = "libc++"; 203 | CLANG_ENABLE_MODULES = YES; 204 | CLANG_ENABLE_OBJC_ARC = YES; 205 | CLANG_WARN_BOOL_CONVERSION = YES; 206 | CLANG_WARN_CONSTANT_CONVERSION = YES; 207 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 208 | CLANG_WARN_EMPTY_BODY = YES; 209 | CLANG_WARN_ENUM_CONVERSION = YES; 210 | CLANG_WARN_INT_CONVERSION = YES; 211 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 212 | CLANG_WARN_UNREACHABLE_CODE = YES; 213 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 214 | COPY_PHASE_STRIP = NO; 215 | CURRENT_PROJECT_VERSION = 1; 216 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 217 | ENABLE_NS_ASSERTIONS = NO; 218 | ENABLE_STRICT_OBJC_MSGSEND = YES; 219 | GCC_C_LANGUAGE_STANDARD = gnu99; 220 | GCC_NO_COMMON_BLOCKS = YES; 221 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 222 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 223 | GCC_WARN_UNDECLARED_SELECTOR = YES; 224 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 225 | GCC_WARN_UNUSED_FUNCTION = YES; 226 | GCC_WARN_UNUSED_VARIABLE = YES; 227 | MACOSX_DEPLOYMENT_TARGET = 10.10; 228 | MTL_ENABLE_DEBUG_INFO = NO; 229 | SDKROOT = macosx; 230 | VERSIONING_SYSTEM = "apple-generic"; 231 | VERSION_INFO_PREFIX = ""; 232 | }; 233 | name = Release; 234 | }; 235 | 6EF11EDC1AF00AD700A7C4BC /* Debug */ = { 236 | isa = XCBuildConfiguration; 237 | buildSettings = { 238 | COMBINE_HIDPI_IMAGES = YES; 239 | DEFINES_MODULE = YES; 240 | DYLIB_COMPATIBILITY_VERSION = 1; 241 | DYLIB_CURRENT_VERSION = 1; 242 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 243 | FRAMEWORK_VERSION = A; 244 | INFOPLIST_FILE = SNRFetchedResultsController/Info.plist; 245 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 246 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; 247 | MACOSX_DEPLOYMENT_TARGET = 10.8; 248 | PRODUCT_NAME = "$(TARGET_NAME)"; 249 | SKIP_INSTALL = YES; 250 | }; 251 | name = Debug; 252 | }; 253 | 6EF11EDD1AF00AD700A7C4BC /* Release */ = { 254 | isa = XCBuildConfiguration; 255 | buildSettings = { 256 | COMBINE_HIDPI_IMAGES = YES; 257 | DEFINES_MODULE = YES; 258 | DYLIB_COMPATIBILITY_VERSION = 1; 259 | DYLIB_CURRENT_VERSION = 1; 260 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 261 | FRAMEWORK_VERSION = A; 262 | INFOPLIST_FILE = SNRFetchedResultsController/Info.plist; 263 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 264 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; 265 | MACOSX_DEPLOYMENT_TARGET = 10.8; 266 | PRODUCT_NAME = "$(TARGET_NAME)"; 267 | SKIP_INSTALL = YES; 268 | }; 269 | name = Release; 270 | }; 271 | /* End XCBuildConfiguration section */ 272 | 273 | /* Begin XCConfigurationList section */ 274 | 6EF11EBF1AF00AD700A7C4BC /* Build configuration list for PBXProject "SNRFetchedResultsController" */ = { 275 | isa = XCConfigurationList; 276 | buildConfigurations = ( 277 | 6EF11ED91AF00AD700A7C4BC /* Debug */, 278 | 6EF11EDA1AF00AD700A7C4BC /* Release */, 279 | ); 280 | defaultConfigurationIsVisible = 0; 281 | defaultConfigurationName = Release; 282 | }; 283 | 6EF11EDB1AF00AD700A7C4BC /* Build configuration list for PBXNativeTarget "SNRFetchedResultsController" */ = { 284 | isa = XCConfigurationList; 285 | buildConfigurations = ( 286 | 6EF11EDC1AF00AD700A7C4BC /* Debug */, 287 | 6EF11EDD1AF00AD700A7C4BC /* Release */, 288 | ); 289 | defaultConfigurationIsVisible = 0; 290 | }; 291 | /* End XCConfigurationList section */ 292 | }; 293 | rootObject = 6EF11EBC1AF00AD700A7C4BC /* Project object */; 294 | } 295 | -------------------------------------------------------------------------------- /SNRFetchedResultsController/SNRFetchedResultsController.xcodeproj/xcuserdata/Coleman.xcuserdatad/xcschemes/SNRFetchedResultsController.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 44 | 45 | 47 | 53 | 54 | 55 | 56 | 57 | 63 | 64 | 65 | 66 | 75 | 76 | 82 | 83 | 84 | 85 | 86 | 87 | 93 | 94 | 100 | 101 | 102 | 103 | 105 | 106 | 109 | 110 | 111 | -------------------------------------------------------------------------------- /SNRFetchedResultsController/SNRFetchedResultsController.xcodeproj/xcuserdata/Coleman.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | SNRFetchedResultsController.xcscheme 8 | 9 | orderHint 10 | 13 11 | 12 | 13 | SuppressBuildableAutocreation 14 | 15 | 6EF11EC41AF00AD700A7C4BC 16 | 17 | primary 18 | 19 | 20 | 6EF11ECF1AF00AD700A7C4BC 21 | 22 | primary 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /SNRFetchedResultsController/SNRFetchedResultsController/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | com.indragiek.$(PRODUCT_NAME:rfc1034identifier) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSHumanReadableCopyright 24 | Copyright © 2015 Indragie Karunaratne. All rights reserved. 25 | NSPrincipalClass 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /SNRFetchedResultsController/SNRFetchedResultsController/SNRFetchedResultsController.h: -------------------------------------------------------------------------------- 1 | // 2 | // SNRFetchedResultsController.h 3 | // Sonora 4 | // 5 | // Created by Indragie Karunaratne on 11-07-11. 6 | // Copyright 2011 Indragie Karunaratne. All rights reserved. 7 | // 8 | 9 | #import 10 | #import 11 | 12 | //! Project version number for SNRFetchedResultsController. 13 | FOUNDATION_EXPORT double SNRFetchedResultsControllerVersionNumber; 14 | 15 | //! Project version string for SNRFetchedResultsController. 16 | FOUNDATION_EXPORT const unsigned char SNRFetchedResultsControllerVersionString[]; 17 | 18 | enum { 19 | SNRFetchedResultsChangeInsert = 1, 20 | SNRFetchedResultsChangeDelete = 2, 21 | SNRFetchedResultsChangeMove = 3, 22 | SNRFetchedResultsChangeUpdate = 4 23 | }; 24 | typedef NSUInteger SNRFetchedResultsChangeType; 25 | 26 | @protocol SNRFetchedResultsControllerDelegate; 27 | @interface SNRFetchedResultsController : NSObject 28 | @property (nonatomic, assign) id delegate; 29 | /** Objects fetched from the managed object context. -performFetch: must be called before accessing fetchedObjects, otherwise a nil array will be returned */ 30 | @property (nonatomic, retain, readonly) NSArray *fetchedObjects; 31 | /** Managed object context and fetch request used to execute the fetch */ 32 | @property (nonatomic, retain, readonly) NSManagedObjectContext *managedObjectContext; 33 | @property (nonatomic, retain, readonly) NSFetchRequest *fetchRequest; 34 | /** 35 | Creates a new SNRFetchedResultsController object with the specified managed object context and fetch request 36 | @param context The managed object context 37 | @param request The fetch request 38 | */ 39 | - (id)initWithManagedObjectContext:(NSManagedObjectContext*)context fetchRequest:(NSFetchRequest*)request; 40 | /** 41 | Performs a fetch to populate the fetchedObjects array. Will immediately return NO if there is no fetchRequest 42 | @param error A pointer to an NSError object that can be used to retrieve more detailed error information in the case of a failure 43 | @return A BOOL indicating whether the fetch was successful 44 | */ 45 | - (BOOL)performFetch:(NSError**)error; 46 | 47 | /** These are just a few wrapper methods to allow easy access to the fetchedObjects array */ 48 | - (id)objectAtIndex:(NSUInteger)index; 49 | - (NSArray*)objectsAtIndexes:(NSIndexSet*)indexes; 50 | - (NSUInteger)indexOfObject:(id)object; 51 | - (NSUInteger)count; 52 | @end 53 | 54 | @protocol SNRFetchedResultsControllerDelegate 55 | @optional 56 | /** 57 | Called right before the controller is about to make one or more changes to the content array 58 | @param controller The fetched results controller 59 | */ 60 | - (void)controllerWillChangeContent:(SNRFetchedResultsController*)controller; 61 | /** 62 | Called right after the controller has made one or more changes to the content array 63 | @param controller The fetched results controller 64 | */ 65 | - (void)controllerDidChangeContent:(SNRFetchedResultsController*)controller; 66 | /** 67 | Called for each change that is made to the content array. This method will be called multiple times throughout the change processing. 68 | @param controller The fetched results controller 69 | @param anObject The object that was updated, deleted, inserted, or moved 70 | @param index The original index of the object. If the object was inserted and did not exist previously, this will be NSNotFound 71 | @param type The type of change (update, insert, delete, or move) 72 | @param newIndex The new index of the object. If the object was deleted, the newIndex will be NSNotFound. 73 | */ 74 | - (void)controller:(SNRFetchedResultsController*)controller didChangeObject:(id)anObject atIndex:(NSUInteger)index forChangeType:(SNRFetchedResultsChangeType)type newIndex:(NSUInteger)newIndex; 75 | @end 76 | -------------------------------------------------------------------------------- /SNRFetchedResultsController/SNRFetchedResultsController/SNRFetchedResultsController.m: -------------------------------------------------------------------------------- 1 | // 2 | // SNRFetchedResultsController.m 3 | // Sonora 4 | // 5 | // Created by Indragie Karunaratne on 11-07-11. 6 | // Copyright 2011 Indragie Karunaratne. All rights reserved. 7 | // 8 | 9 | #import "SNRFetchedResultsController.h" 10 | 11 | @interface SNRFetchedResultsController () 12 | - (void)managedObjectContextObjectsDidChange:(NSNotification*)notification; 13 | - (void)delegateDidChangeObject:(id)anObject atIndex:(NSUInteger)index forChangeType:(SNRFetchedResultsChangeType)type newIndex:(NSUInteger)newIndex; 14 | - (void)delegateWillChangeContent; 15 | - (void)delegateDidChangeContent; 16 | @end 17 | 18 | @interface SNRFetchedResultsUpdate : NSObject 19 | @property (nonatomic, retain) NSManagedObject *object; 20 | @property (nonatomic, assign) NSUInteger originalIndex; 21 | @end 22 | 23 | @implementation SNRFetchedResultsController { 24 | NSMutableArray *sFetchedObjects; 25 | BOOL sDidCallDelegateWillChangeContent; 26 | 27 | struct { 28 | BOOL delegateHasWillChangeContent; 29 | BOOL delegateHasDidChangeContent; 30 | BOOL delegateHasDidChangeObject; 31 | } sDelegateHas; 32 | } 33 | @synthesize fetchedObjects = sFetchedObjects; 34 | @synthesize fetchRequest = sFetchRequest; 35 | @synthesize managedObjectContext = sManagedObjectContext; 36 | @synthesize delegate = sDelegate; 37 | 38 | #pragma mark - 39 | #pragma mark Initialization 40 | 41 | - (id)initWithManagedObjectContext:(NSManagedObjectContext*)context fetchRequest:(NSFetchRequest *)request 42 | { 43 | if ((self = [super init])) { 44 | sManagedObjectContext = context; 45 | sFetchRequest = request; 46 | [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(managedObjectContextObjectsDidChange:) name:NSManagedObjectContextObjectsDidChangeNotification object:sManagedObjectContext]; 47 | } 48 | return self; 49 | } 50 | 51 | #pragma mark - 52 | #pragma mark Fetched Objects 53 | 54 | - (BOOL)performFetch:(NSError**)error 55 | { 56 | if (!self.fetchRequest) { return NO; } 57 | sFetchedObjects = [NSMutableArray arrayWithArray:[self.managedObjectContext executeFetchRequest:self.fetchRequest error:error]]; 58 | return (sFetchedObjects != nil); 59 | } 60 | 61 | - (id)objectAtIndex:(NSUInteger)index 62 | { 63 | return [sFetchedObjects objectAtIndex:index]; 64 | } 65 | 66 | - (NSArray*)objectsAtIndexes:(NSIndexSet*)indexes 67 | { 68 | return [sFetchedObjects objectsAtIndexes:indexes]; 69 | } 70 | 71 | - (NSUInteger)indexOfObject:(id)object 72 | { 73 | return [sFetchedObjects indexOfObject:object]; 74 | } 75 | 76 | - (NSUInteger)count 77 | { 78 | return [sFetchedObjects count]; 79 | } 80 | 81 | #pragma mark - Accessors 82 | 83 | - (void)setDelegate:(id)delegate 84 | { 85 | sDelegate = delegate; 86 | sDelegateHas.delegateHasWillChangeContent = [sDelegate respondsToSelector:@selector(controllerWillChangeContent:)]; 87 | sDelegateHas.delegateHasDidChangeContent = [sDelegate respondsToSelector:@selector(controllerDidChangeContent:)]; 88 | sDelegateHas.delegateHasDidChangeObject = [sDelegate respondsToSelector:@selector(controller:didChangeObject:atIndex:forChangeType:newIndex:)]; 89 | } 90 | 91 | #pragma mark - 92 | #pragma mark Memory Management 93 | 94 | - (void)dealloc 95 | { 96 | [[NSNotificationCenter defaultCenter] removeObserver:self]; 97 | } 98 | 99 | #pragma mark - 100 | #pragma mark Private 101 | 102 | // Gathered most of the details for this method based on information from Apple docs on NSFetchedResultsController 103 | // 104 | // and NSFetchedResultsControllerDelegate 105 | // 106 | 107 | - (void)managedObjectContextObjectsDidChange:(NSNotification*)notification 108 | { 109 | if (!self.fetchRequest) { return; } 110 | NSDictionary *userInfo = [notification userInfo]; 111 | NSPredicate *predicate = [self.fetchRequest predicate]; 112 | NSEntityDescription *entity = [self.fetchRequest entity]; 113 | NSArray *sortDescriptors = [self.fetchRequest sortDescriptors]; 114 | NSArray *sortKeys = [sortDescriptors valueForKey:@"key"]; 115 | // Are arrays faster to enumerate than sets? 116 | NSArray *insertedObjects = [[userInfo valueForKey:NSInsertedObjectsKey] allObjects]; 117 | NSArray *updatedObjects = [[userInfo valueForKey:NSUpdatedObjectsKey] allObjects]; 118 | NSArray *deletedObjects = [[userInfo valueForKey:NSDeletedObjectsKey] allObjects]; 119 | NSMutableArray *inserted = [NSMutableArray array]; // objects to insert and sort at the end 120 | NSMutableArray *updated = [NSMutableArray array]; // updated objects that change the sorting of the array. Updated objects that do not affect sorting will be updated immediately instead of being put in this array 121 | sDidCallDelegateWillChangeContent = NO; 122 | for (NSManagedObject *object in deletedObjects) { 123 | // Don't care about objects of a different entity 124 | if (![[object entity] isKindOfEntity:entity]) { continue; } 125 | // Check to see if the content array contains the deleted object 126 | NSUInteger index = [sFetchedObjects indexOfObject:object]; 127 | if (index == NSNotFound) { continue; } 128 | [sFetchedObjects removeObjectAtIndex:index]; 129 | [self delegateDidChangeObject:object atIndex:index forChangeType:SNRFetchedResultsChangeDelete newIndex:NSNotFound]; 130 | } 131 | for (NSManagedObject *object in updatedObjects) { 132 | // Ignore objects of a different entity 133 | if (![[object entity] isKindOfEntity:entity]) { continue; } 134 | // Check to see if the predicate evaluates regardless of whether the object exists in the content array or not 135 | // because changes to the attributes of the object can result in it either being removed or added to the 136 | // content array depending on whether it affects the evaluation of the predicate 137 | BOOL predicateEvaluates = (predicate != nil) ? [predicate evaluateWithObject:object] : YES; 138 | NSUInteger objectIndex = [self.fetchedObjects indexOfObject:object]; 139 | BOOL containsObject = (objectIndex != NSNotFound); 140 | // If the content array already contains the object but the update resulted in the predicate 141 | // no longer evaluating to TRUE, then it needs to be removed 142 | if (containsObject && !predicateEvaluates) { 143 | [sFetchedObjects removeObjectAtIndex:objectIndex]; 144 | [self delegateDidChangeObject:object atIndex:objectIndex forChangeType:SNRFetchedResultsChangeDelete newIndex:NSNotFound]; 145 | // If the content array does not contain the object but the object's update resulted in the predicate now 146 | // evaluating to TRUE, then it needs to be inserted 147 | } else if (!containsObject && predicateEvaluates) { 148 | [inserted addObject:object]; 149 | } else if (containsObject) { 150 | // Check if the object's updated keys are in the sort keys 151 | // This means that the sorting would have to be updated 152 | BOOL sortingChanged = NO; 153 | if ([sortKeys count]) { 154 | NSArray *keys = [[object changedValues] allKeys]; 155 | for (NSString *key in sortKeys) { 156 | if ([keys containsObject:key]) { 157 | sortingChanged = YES; 158 | break; 159 | } 160 | } 161 | } 162 | if (sortingChanged) { 163 | // Create a wrapper object that keeps track of the original index for later 164 | SNRFetchedResultsUpdate *update = [SNRFetchedResultsUpdate new]; 165 | update.originalIndex = objectIndex; 166 | update.object = object; 167 | [updated addObject:update]; 168 | } else { 169 | // If there's no change in sorting then just update the object as-is 170 | [self delegateDidChangeObject:object atIndex:objectIndex forChangeType:SNRFetchedResultsChangeUpdate newIndex:objectIndex]; 171 | } 172 | } 173 | } 174 | // If there were updated objects that changed the sorting then resort and notify the delegate of changes 175 | if ([updated count] && [sortDescriptors count]) { 176 | [sFetchedObjects sortUsingDescriptors:sortDescriptors]; 177 | for (SNRFetchedResultsUpdate *update in updated) { 178 | // Find out then new index of the object in the content array 179 | NSUInteger newIndex = [sFetchedObjects indexOfObject:update.object]; 180 | [self delegateDidChangeObject:update.object atIndex:update.originalIndex forChangeType:SNRFetchedResultsChangeMove newIndex:newIndex]; 181 | } 182 | } 183 | for (NSManagedObject *object in insertedObjects) { 184 | // Objects of a different entity or objects that don't evaluate to the predicate are ignored 185 | if (![[object entity] isKindOfEntity:entity] || (predicate && ![predicate evaluateWithObject:object])) { 186 | continue; 187 | } 188 | [inserted addObject:object]; 189 | } 190 | // If there were inserted objects then insert them into the content array and resort 191 | NSUInteger insertedCount = [inserted count]; 192 | if (insertedCount) { 193 | // Dump the inserted objects into the content array 194 | [sFetchedObjects addObjectsFromArray:inserted]; 195 | // If there are sort descriptors, then resort the array 196 | if ([sortDescriptors count]) { 197 | [sFetchedObjects sortUsingDescriptors:sortDescriptors]; 198 | // Enumerate through each of the inserted objects and notify the delegate of their new position 199 | [sFetchedObjects enumerateObjectsUsingBlock:^(NSManagedObject *object, NSUInteger idx, BOOL *stop) { 200 | if (![inserted containsObject:object]) { 201 | return; 202 | } 203 | 204 | [self delegateDidChangeObject:object atIndex:NSNotFound forChangeType:SNRFetchedResultsChangeInsert newIndex:idx]; 205 | }]; 206 | // If there are no sort descriptors, then the inserted objects will just be added to the end of the array 207 | // so we don't need to figure out what indexes they were inserted in 208 | } else { 209 | NSUInteger objectsCount = [sFetchedObjects count]; 210 | for (NSInteger i = (objectsCount - insertedCount); i < objectsCount; i++) { 211 | [self delegateDidChangeObject:[sFetchedObjects objectAtIndex:0] atIndex:NSNotFound forChangeType:SNRFetchedResultsChangeInsert newIndex:i]; 212 | } 213 | } 214 | } 215 | // if delegateWillChangeContent: was called then delegateDidChangeContent: must also be called 216 | if (sDidCallDelegateWillChangeContent) { 217 | [self delegateDidChangeContent]; 218 | } 219 | } 220 | 221 | - (void)delegateWillChangeContent 222 | { 223 | if (sDelegateHas.delegateHasWillChangeContent) { 224 | [self.delegate controllerWillChangeContent:self]; 225 | } 226 | } 227 | 228 | - (void)delegateDidChangeContent 229 | { 230 | if (sDelegateHas.delegateHasDidChangeContent) { 231 | [self.delegate controllerDidChangeContent:self]; 232 | } 233 | } 234 | 235 | - (void)delegateDidChangeObject:(id)anObject atIndex:(NSUInteger)index forChangeType:(SNRFetchedResultsChangeType)type newIndex:(NSUInteger)newIndex 236 | { 237 | // NSLog(@"Changing object: %@\nAt index: %lu\nChange type: %d\nNew index: %lu", anObject, index, (int)type, newIndex); 238 | if (!sDidCallDelegateWillChangeContent) { 239 | [self delegateWillChangeContent]; 240 | sDidCallDelegateWillChangeContent = YES; 241 | } 242 | if (sDelegateHas.delegateHasDidChangeObject) { 243 | [self.delegate controller:self didChangeObject:anObject atIndex:index forChangeType:type newIndex:newIndex]; 244 | } 245 | } 246 | @end 247 | 248 | @implementation SNRFetchedResultsUpdate 249 | @synthesize object = sObject; 250 | @synthesize originalIndex = sOriginalIndex; 251 | @end --------------------------------------------------------------------------------