├── .gitignore ├── .gitmodules ├── LICENSE.md ├── README.md ├── ReactiveCoreData.podspec ├── ReactiveCoreData.xcodeproj └── project.pbxproj ├── ReactiveCoreData ├── NSManagedObject+ReactiveCoreData.h ├── NSManagedObject+ReactiveCoreData.m ├── NSManagedObjectContext+ReactiveCoreData.h ├── NSManagedObjectContext+ReactiveCoreData.m ├── RACSignal+ReactiveCoreData.h ├── RACSignal+ReactiveCoreData.m └── ReactiveCoreData.h ├── ReactiveCoreDataApp ├── ASWAppDelegate.h ├── ASWAppDelegate.m ├── Parent.h ├── Parent.m ├── ReactiveCoreData-Info.plist ├── ReactiveCoreData-Prefix.pch ├── ReactiveCoreData.xcdatamodeld │ ├── .xccurrentversion │ └── ReactiveCoreData.xcdatamodel │ │ └── contents ├── en.lproj │ ├── Credits.rtf │ ├── InfoPlist.strings │ └── MainMenu.xib └── main.m └── ReactiveCoreDataTests ├── RACManagedObjectFetchSpecs.m ├── ReactiveCoreDataTests-Info.plist └── en.lproj └── InfoPlist.strings /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | build/* 3 | *.pbxuser 4 | !default.pbxuser 5 | *.mode1v3 6 | !default.mode1v3 7 | *.mode2v3 8 | !default.mode2v3 9 | *.perspectivev3 10 | !default.perspectivev3 11 | *.xcworkspace 12 | !default.xcworkspace 13 | xcuserdata 14 | profile 15 | .idea 16 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "External/ReactiveCocoa"] 2 | path = External/ReactiveCocoa 3 | url = https://github.com/ReactiveCocoa/ReactiveCocoa.git 4 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | **Copyright (c) 2013 Apparent Software.** 2 | **All rights reserved.** 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | this software and associated documentation files (the "Software"), to deal in 6 | the Software without restriction, including without limitation the rights to 7 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | the Software, and to permit persons to whom the Software is furnished to do so, 9 | subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ReactiveCoreData 2 | 3 | ReactiveCoreData (RCD) is an attempt to bring Core Data into the [ReactiveCocoa](https://github.com/ReactiveCocoa/ReactiveCocoa) (RAC) world. 4 | 5 | Currently has several files with the source code, [Specta](https://github.com/petejkim/specta) specs and a [demo application][Demo] for the Mac. 6 | 7 | To use, copy the source files from [ReactiveCoreData](ReactiveCoreData) folder to your project. You should also have ReactiveCocoa in your project, of course. 8 | 9 | Code from the Mac example: 10 | 11 | ```objc 12 | // This part refetches data for the table and puts it into filteredParents 13 | // It either fetches all Parents or filters by name, if there's something in the search field 14 | // It will also refetch, if objectsChanged send a next 15 | RAC(self.filteredParents) = [[[[Parent findAll] 16 | where:@"name" contains:filterText options:@"cd"] 17 | sortBy:@"name"] 18 | fetchWithTrigger:objectsChanged]; 19 | ``` 20 | 21 | Another example of background processing: 22 | ```objc 23 | [[[[triggerSignal 24 | performInBackgroundContext:^(NSManagedObjectContext *context) { 25 | [Parent insert]; 26 | }] 27 | saveContext] 28 | deliverOn:RACScheduler.mainThreadScheduler] 29 | subscribeNext:^(id _) { 30 | // Update UI 31 | }]; 32 | 33 | // We can also react to main context's merge notifications to update the UI 34 | [mainContext.rcd_merged 35 | subscribeNext:^(NSNotification *note){ 36 | // Update UI 37 | }]; 38 | ``` 39 | 40 | See the test [Specs][Specs] for some crude usage examples. 41 | 42 | Also checkout the demo application in the project. It shows a simple table-view with Core Data backed storage using ReactiveCoreData and ReactiveCocoa for connecting things. 43 | 44 | The headers also provide documentation for the various methods. 45 | 46 | It's not feature-complete and more could be done but will be added based on actual usage and your contributions. 47 | 48 | That being said, it should work both with shoebox and document-based applications, where there are many object contexts. 49 | 50 | 51 | ### Done: 52 | 53 | - Start signals that represent and modify NSFetchRequests (findAll, findOne) from NSManagedObject. 54 | - `-[RACSignal where:args:]` method that sets a predicate with given format that can have signals as its arguments. This brings execution of NSFetchRequests into the Reactive domain. As any signal to predicate changes, the query is modified and sent next — to be fetched, for example. 55 | - A couple of signal-aware convenience methods for common predicate cases, like for CONTAINS predicate and for equal 56 | - `[RACSignal limit:]` that accepts either a value or a signal. 57 | - `[RACSignal sortBy:]` that accepts either a "key" string, or a "-key" (for descending sort), or a sort descriptor, or an array of sort descriptors, or a signal with any of these 58 | - `fetch` and `count` methods on RACSignal to execute the NSFetchRequest that's passed to them in the current NSManagedObjectContext and send the resulting array (or count) as "next". 59 | - `fetchWithTrigger:` which will rerun the fetch if the trigger fires any value. 60 | - fetching objectID and converting objectIDs to objects in current context 61 | - Running in background contexts 62 | - Saving of non-main contexts merges them into the main context. 63 | - Signal that's fired when a context is saved (wraps NSManagedObjectContextDidSaveNotification). 64 | - Signal that's fired after a merge. 65 | - Support not only for shoebox applications (with one main managed object context) but also document-based applications where you have a separate context for each document. 66 | 67 | [Demo]: ReactiveCoreDataApp/ASWAppDelegate.m 68 | [Specs]: ReactiveCoreDataTests/RACManagedObjectFetchSpecs.m 69 | -------------------------------------------------------------------------------- /ReactiveCoreData.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "ReactiveCoreData" 3 | s.version = "0.0.1" 4 | s.summary = "ReactiveCoreData (RCD) is an attempt to bring Core Data into the ReactiveCocoa (RAC) world." 5 | s.homepage = "https://github.com/apparentsoft/ReactiveCoreData.git" 6 | s.author = { "Jacob Gorban" => "apparentsoft" } 7 | s.source = { :git => "https://github.com/apparentsoft/ReactiveCoreData.git", :tag => "#{s.version}" } 8 | s.license = 'Simplified BSD License' 9 | s.description = "ReactiveCoreData (RCD) is an attempt to bring Core Data into the ReactiveCocoa (RAC) world." 10 | 11 | s.requires_arc = true 12 | s.ios.deployment_target = '6.0' 13 | s.osx.deployment_target = '10.7' 14 | 15 | s.source_files = 'ReactiveCoreData/*.{h,m}' 16 | s.dependency 'ReactiveCocoa' 17 | 18 | s.ios.frameworks = 'Foundation', 'UIKit', 'CoreData' 19 | s.osx.frameworks = 'Foundation', 'Cocoa', 'CoreData' 20 | 21 | end 22 | -------------------------------------------------------------------------------- /ReactiveCoreData.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 166738681729695E00CABF2C /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 166738671729695E00CABF2C /* Cocoa.framework */; }; 11 | 166738721729695E00CABF2C /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 166738701729695E00CABF2C /* InfoPlist.strings */; }; 12 | 166738741729695E00CABF2C /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 166738731729695E00CABF2C /* main.m */; }; 13 | 166738781729695E00CABF2C /* Credits.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 166738761729695E00CABF2C /* Credits.rtf */; }; 14 | 1667387B1729695E00CABF2C /* ASWAppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 1667387A1729695E00CABF2C /* ASWAppDelegate.m */; }; 15 | 1667387E1729695E00CABF2C /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 1667387C1729695E00CABF2C /* MainMenu.xib */; }; 16 | 166738811729695E00CABF2C /* ReactiveCoreData.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 1667387F1729695E00CABF2C /* ReactiveCoreData.xcdatamodeld */; }; 17 | 166738891729695E00CABF2C /* SenTestingKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 166738881729695E00CABF2C /* SenTestingKit.framework */; }; 18 | 1667388A1729695E00CABF2C /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 166738671729695E00CABF2C /* Cocoa.framework */; }; 19 | 166738921729695E00CABF2C /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 166738901729695E00CABF2C /* InfoPlist.strings */; }; 20 | 166738BD1729700300CABF2C /* RACManagedObjectFetchSpecs.m in Sources */ = {isa = PBXBuildFile; fileRef = 166738BC1729700300CABF2C /* RACManagedObjectFetchSpecs.m */; }; 21 | 1690734817332229007186EE /* CoreData.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1667386B1729695E00CABF2C /* CoreData.framework */; }; 22 | 1690735117339D3E007186EE /* NSManagedObject+ReactiveCoreData.m in Sources */ = {isa = PBXBuildFile; fileRef = 166738B917296FBE00CABF2C /* NSManagedObject+ReactiveCoreData.m */; }; 23 | 1690735217339D3E007186EE /* RACSignal+ReactiveCoreData.m in Sources */ = {isa = PBXBuildFile; fileRef = 16AF98F817297EBF000E9B8B /* RACSignal+ReactiveCoreData.m */; }; 24 | 1690735317339D3E007186EE /* NSManagedObjectContext+ReactiveCoreData.m in Sources */ = {isa = PBXBuildFile; fileRef = 16CEF482172981D700921072 /* NSManagedObjectContext+ReactiveCoreData.m */; }; 25 | 1690735417339D4B007186EE /* ReactiveCocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 166738A917296D0200CABF2C /* ReactiveCocoa.framework */; }; 26 | 1690735517339D68007186EE /* ReactiveCocoa.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = 166738A917296D0200CABF2C /* ReactiveCocoa.framework */; }; 27 | 16BE18E71729788E00096F6C /* libExpecta.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 16BE18CD1729786400096F6C /* libExpecta.a */; }; 28 | 16BE18E81729788E00096F6C /* libSpecta.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 16BE18DE1729787F00096F6C /* libSpecta.a */; }; 29 | 16CEF48E172ACD7500921072 /* Parent.m in Sources */ = {isa = PBXBuildFile; fileRef = 16CEF48D172ACD7500921072 /* Parent.m */; }; 30 | 16CEF49C172D2A1100921072 /* ReactiveCocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 166738A917296D0200CABF2C /* ReactiveCocoa.framework */; }; 31 | 16CEF49F172D2A2800921072 /* ReactiveCocoa.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = 166738A917296D0200CABF2C /* ReactiveCocoa.framework */; }; 32 | /* End PBXBuildFile section */ 33 | 34 | /* Begin PBXContainerItemProxy section */ 35 | 1667388B1729695E00CABF2C /* PBXContainerItemProxy */ = { 36 | isa = PBXContainerItemProxy; 37 | containerPortal = 1667385C1729695E00CABF2C /* Project object */; 38 | proxyType = 1; 39 | remoteGlobalIDString = 166738631729695E00CABF2C; 40 | remoteInfo = ReactiveCoreData; 41 | }; 42 | 166738A817296D0200CABF2C /* PBXContainerItemProxy */ = { 43 | isa = PBXContainerItemProxy; 44 | containerPortal = 166738A017296D0200CABF2C /* ReactiveCocoa.xcodeproj */; 45 | proxyType = 2; 46 | remoteGlobalIDString = 88037F8315056328001A5B19; 47 | remoteInfo = ReactiveCocoa; 48 | }; 49 | 166738AA17296D0200CABF2C /* PBXContainerItemProxy */ = { 50 | isa = PBXContainerItemProxy; 51 | containerPortal = 166738A017296D0200CABF2C /* ReactiveCocoa.xcodeproj */; 52 | proxyType = 2; 53 | remoteGlobalIDString = 88F440AB153DAC820097B4C3; 54 | remoteInfo = "ReactiveCocoa-iOS"; 55 | }; 56 | 166738AC17296D0200CABF2C /* PBXContainerItemProxy */ = { 57 | isa = PBXContainerItemProxy; 58 | containerPortal = 166738A017296D0200CABF2C /* ReactiveCocoa.xcodeproj */; 59 | proxyType = 2; 60 | remoteGlobalIDString = 88CDF7DC15000FCF00163A9F; 61 | remoteInfo = ReactiveCocoaTests; 62 | }; 63 | 1690735617339D6C007186EE /* PBXContainerItemProxy */ = { 64 | isa = PBXContainerItemProxy; 65 | containerPortal = 166738A017296D0200CABF2C /* ReactiveCocoa.xcodeproj */; 66 | proxyType = 1; 67 | remoteGlobalIDString = 88037F8215056328001A5B19; 68 | remoteInfo = ReactiveCocoa; 69 | }; 70 | 16BE18CC1729786400096F6C /* PBXContainerItemProxy */ = { 71 | isa = PBXContainerItemProxy; 72 | containerPortal = 16BE18C51729786400096F6C /* Expecta.xcodeproj */; 73 | proxyType = 2; 74 | remoteGlobalIDString = E9ACDF0C13B2DD520010F4D7; 75 | remoteInfo = Expecta; 76 | }; 77 | 16BE18CE1729786400096F6C /* PBXContainerItemProxy */ = { 78 | isa = PBXContainerItemProxy; 79 | containerPortal = 16BE18C51729786400096F6C /* Expecta.xcodeproj */; 80 | proxyType = 2; 81 | remoteGlobalIDString = E93067CE13B2E6D100EA26FF; 82 | remoteInfo = "Expecta-iOS"; 83 | }; 84 | 16BE18D01729786400096F6C /* PBXContainerItemProxy */ = { 85 | isa = PBXContainerItemProxy; 86 | containerPortal = 16BE18C51729786400096F6C /* Expecta.xcodeproj */; 87 | proxyType = 2; 88 | remoteGlobalIDString = E9ACDF1D13B2DD520010F4D7; 89 | remoteInfo = ExpectaTests; 90 | }; 91 | 16BE18D21729786400096F6C /* PBXContainerItemProxy */ = { 92 | isa = PBXContainerItemProxy; 93 | containerPortal = 16BE18C51729786400096F6C /* Expecta.xcodeproj */; 94 | proxyType = 2; 95 | remoteGlobalIDString = E93067DA13B2E6D100EA26FF; 96 | remoteInfo = "Expecta-iOSTests"; 97 | }; 98 | 16BE18D41729786B00096F6C /* PBXContainerItemProxy */ = { 99 | isa = PBXContainerItemProxy; 100 | containerPortal = 16BE18C51729786400096F6C /* Expecta.xcodeproj */; 101 | proxyType = 1; 102 | remoteGlobalIDString = E9ACDF0B13B2DD520010F4D7; 103 | remoteInfo = Expecta; 104 | }; 105 | 16BE18DD1729787F00096F6C /* PBXContainerItemProxy */ = { 106 | isa = PBXContainerItemProxy; 107 | containerPortal = 16BE18D61729787F00096F6C /* Specta.xcodeproj */; 108 | proxyType = 2; 109 | remoteGlobalIDString = E9D96A2614B6B8AB007D9521; 110 | remoteInfo = Specta; 111 | }; 112 | 16BE18DF1729787F00096F6C /* PBXContainerItemProxy */ = { 113 | isa = PBXContainerItemProxy; 114 | containerPortal = 16BE18D61729787F00096F6C /* Specta.xcodeproj */; 115 | proxyType = 2; 116 | remoteGlobalIDString = E9B777A414BA294B00D8DC76; 117 | remoteInfo = "Specta-iOS"; 118 | }; 119 | 16BE18E11729787F00096F6C /* PBXContainerItemProxy */ = { 120 | isa = PBXContainerItemProxy; 121 | containerPortal = 16BE18D61729787F00096F6C /* Specta.xcodeproj */; 122 | proxyType = 2; 123 | remoteGlobalIDString = E9D96A3A14B6B8AB007D9521; 124 | remoteInfo = SpectaTests; 125 | }; 126 | 16BE18E31729787F00096F6C /* PBXContainerItemProxy */ = { 127 | isa = PBXContainerItemProxy; 128 | containerPortal = 16BE18D61729787F00096F6C /* Specta.xcodeproj */; 129 | proxyType = 2; 130 | remoteGlobalIDString = E9B777B314BA294C00D8DC76; 131 | remoteInfo = "Specta-iOSTests"; 132 | }; 133 | 16BE18E51729788400096F6C /* PBXContainerItemProxy */ = { 134 | isa = PBXContainerItemProxy; 135 | containerPortal = 16BE18D61729787F00096F6C /* Specta.xcodeproj */; 136 | proxyType = 1; 137 | remoteGlobalIDString = E9D96A2514B6B8AB007D9521; 138 | remoteInfo = Specta; 139 | }; 140 | 16CEF49D172D2A1800921072 /* PBXContainerItemProxy */ = { 141 | isa = PBXContainerItemProxy; 142 | containerPortal = 166738A017296D0200CABF2C /* ReactiveCocoa.xcodeproj */; 143 | proxyType = 1; 144 | remoteGlobalIDString = 88037F8215056328001A5B19; 145 | remoteInfo = ReactiveCocoa; 146 | }; 147 | 80102BBF18903EDB002F25AE /* PBXContainerItemProxy */ = { 148 | isa = PBXContainerItemProxy; 149 | containerPortal = 166738A017296D0200CABF2C /* ReactiveCocoa.xcodeproj */; 150 | proxyType = 2; 151 | remoteGlobalIDString = 5FAF5223174D4C2000CAC810; 152 | remoteInfo = "ReactiveCocoaTests-iOS"; 153 | }; 154 | 80102BC118903EDB002F25AE /* PBXContainerItemProxy */ = { 155 | isa = PBXContainerItemProxy; 156 | containerPortal = 166738A017296D0200CABF2C /* ReactiveCocoa.xcodeproj */; 157 | proxyType = 2; 158 | remoteGlobalIDString = 1860F412177C91B500C7B3C9; 159 | remoteInfo = "ReactiveCocoa-iOS-UIKitTestHost"; 160 | }; 161 | 80102BC318903EDB002F25AE /* PBXContainerItemProxy */ = { 162 | isa = PBXContainerItemProxy; 163 | containerPortal = 166738A017296D0200CABF2C /* ReactiveCocoa.xcodeproj */; 164 | proxyType = 2; 165 | remoteGlobalIDString = 1860F430177C91B500C7B3C9; 166 | remoteInfo = "ReactiveCocoa-iOS-UIKitTestHostTests"; 167 | }; 168 | 80102BC518903EDB002F25AE /* PBXContainerItemProxy */ = { 169 | isa = PBXContainerItemProxy; 170 | containerPortal = 166738A017296D0200CABF2C /* ReactiveCocoa.xcodeproj */; 171 | proxyType = 2; 172 | remoteGlobalIDString = D05AD39417F2D56F0080895B; 173 | remoteInfo = "ReactiveCocoa-Mac"; 174 | }; 175 | /* End PBXContainerItemProxy section */ 176 | 177 | /* Begin PBXCopyFilesBuildPhase section */ 178 | 166738AF17296D4300CABF2C /* CopyFiles */ = { 179 | isa = PBXCopyFilesBuildPhase; 180 | buildActionMask = 2147483647; 181 | dstPath = ""; 182 | dstSubfolderSpec = 10; 183 | files = ( 184 | 1690735517339D68007186EE /* ReactiveCocoa.framework in CopyFiles */, 185 | ); 186 | runOnlyForDeploymentPostprocessing = 0; 187 | }; 188 | 16CEF498172D25B300921072 /* CopyFiles */ = { 189 | isa = PBXCopyFilesBuildPhase; 190 | buildActionMask = 2147483647; 191 | dstPath = ""; 192 | dstSubfolderSpec = 10; 193 | files = ( 194 | 16CEF49F172D2A2800921072 /* ReactiveCocoa.framework in CopyFiles */, 195 | ); 196 | runOnlyForDeploymentPostprocessing = 0; 197 | }; 198 | /* End PBXCopyFilesBuildPhase section */ 199 | 200 | /* Begin PBXFileReference section */ 201 | 1637E1D817328BEA0095B22B /* ReactiveCoreData.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ReactiveCoreData.h; sourceTree = ""; }; 202 | 166738641729695E00CABF2C /* ReactiveCoreData.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ReactiveCoreData.app; sourceTree = BUILT_PRODUCTS_DIR; }; 203 | 166738671729695E00CABF2C /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = System/Library/Frameworks/Cocoa.framework; sourceTree = SDKROOT; }; 204 | 1667386A1729695E00CABF2C /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = System/Library/Frameworks/AppKit.framework; sourceTree = SDKROOT; }; 205 | 1667386B1729695E00CABF2C /* CoreData.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreData.framework; path = System/Library/Frameworks/CoreData.framework; sourceTree = SDKROOT; }; 206 | 1667386C1729695E00CABF2C /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; 207 | 1667386F1729695E00CABF2C /* ReactiveCoreData-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "ReactiveCoreData-Info.plist"; sourceTree = ""; }; 208 | 166738711729695E00CABF2C /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; 209 | 166738731729695E00CABF2C /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 210 | 166738751729695E00CABF2C /* ReactiveCoreData-Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "ReactiveCoreData-Prefix.pch"; sourceTree = ""; }; 211 | 166738771729695E00CABF2C /* en */ = {isa = PBXFileReference; lastKnownFileType = text.rtf; name = en; path = en.lproj/Credits.rtf; sourceTree = ""; }; 212 | 166738791729695E00CABF2C /* ASWAppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ASWAppDelegate.h; sourceTree = ""; }; 213 | 1667387A1729695E00CABF2C /* ASWAppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ASWAppDelegate.m; sourceTree = ""; }; 214 | 1667387D1729695E00CABF2C /* en */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = en; path = en.lproj/MainMenu.xib; sourceTree = ""; }; 215 | 166738801729695E00CABF2C /* ReactiveCoreData.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = ReactiveCoreData.xcdatamodel; sourceTree = ""; }; 216 | 166738871729695E00CABF2C /* ReactiveCoreDataTests.octest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ReactiveCoreDataTests.octest; sourceTree = BUILT_PRODUCTS_DIR; }; 217 | 166738881729695E00CABF2C /* SenTestingKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SenTestingKit.framework; path = Library/Frameworks/SenTestingKit.framework; sourceTree = DEVELOPER_DIR; }; 218 | 1667388F1729695E00CABF2C /* ReactiveCoreDataTests-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "ReactiveCoreDataTests-Info.plist"; sourceTree = ""; }; 219 | 166738911729695E00CABF2C /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; 220 | 166738A017296D0200CABF2C /* ReactiveCocoa.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = ReactiveCocoa.xcodeproj; path = External/ReactiveCocoa/ReactiveCocoaFramework/ReactiveCocoa.xcodeproj; sourceTree = ""; }; 221 | 166738B817296FBE00CABF2C /* NSManagedObject+ReactiveCoreData.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSManagedObject+ReactiveCoreData.h"; sourceTree = ""; }; 222 | 166738B917296FBE00CABF2C /* NSManagedObject+ReactiveCoreData.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSManagedObject+ReactiveCoreData.m"; sourceTree = ""; }; 223 | 166738BC1729700300CABF2C /* RACManagedObjectFetchSpecs.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RACManagedObjectFetchSpecs.m; sourceTree = ""; }; 224 | 16AF98F717297EBF000E9B8B /* RACSignal+ReactiveCoreData.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "RACSignal+ReactiveCoreData.h"; sourceTree = ""; }; 225 | 16AF98F817297EBF000E9B8B /* RACSignal+ReactiveCoreData.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "RACSignal+ReactiveCoreData.m"; sourceTree = ""; usesTabs = 0; }; 226 | 16BE18C51729786400096F6C /* Expecta.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = Expecta.xcodeproj; path = External/ReactiveCocoa/external/expecta/Expecta.xcodeproj; sourceTree = ""; }; 227 | 16BE18D61729787F00096F6C /* Specta.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = Specta.xcodeproj; path = External/ReactiveCocoa/external/specta/Specta.xcodeproj; sourceTree = ""; }; 228 | 16CEF481172981D700921072 /* NSManagedObjectContext+ReactiveCoreData.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSManagedObjectContext+ReactiveCoreData.h"; sourceTree = ""; }; 229 | 16CEF482172981D700921072 /* NSManagedObjectContext+ReactiveCoreData.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSManagedObjectContext+ReactiveCoreData.m"; sourceTree = ""; }; 230 | 16CEF48C172ACD7500921072 /* Parent.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Parent.h; sourceTree = ""; }; 231 | 16CEF48D172ACD7500921072 /* Parent.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Parent.m; sourceTree = ""; }; 232 | /* End PBXFileReference section */ 233 | 234 | /* Begin PBXFrameworksBuildPhase section */ 235 | 166738611729695E00CABF2C /* Frameworks */ = { 236 | isa = PBXFrameworksBuildPhase; 237 | buildActionMask = 2147483647; 238 | files = ( 239 | 1690735417339D4B007186EE /* ReactiveCocoa.framework in Frameworks */, 240 | 1690734817332229007186EE /* CoreData.framework in Frameworks */, 241 | 166738681729695E00CABF2C /* Cocoa.framework in Frameworks */, 242 | ); 243 | runOnlyForDeploymentPostprocessing = 0; 244 | }; 245 | 166738831729695E00CABF2C /* Frameworks */ = { 246 | isa = PBXFrameworksBuildPhase; 247 | buildActionMask = 2147483647; 248 | files = ( 249 | 16CEF49C172D2A1100921072 /* ReactiveCocoa.framework in Frameworks */, 250 | 16BE18E71729788E00096F6C /* libExpecta.a in Frameworks */, 251 | 16BE18E81729788E00096F6C /* libSpecta.a in Frameworks */, 252 | 166738891729695E00CABF2C /* SenTestingKit.framework in Frameworks */, 253 | 1667388A1729695E00CABF2C /* Cocoa.framework in Frameworks */, 254 | ); 255 | runOnlyForDeploymentPostprocessing = 0; 256 | }; 257 | /* End PBXFrameworksBuildPhase section */ 258 | 259 | /* Begin PBXGroup section */ 260 | 1667385B1729695E00CABF2C = { 261 | isa = PBXGroup; 262 | children = ( 263 | 1667389F17296A8E00CABF2C /* External */, 264 | 1667389E17296A8400CABF2C /* ReactiveCoreData */, 265 | 1667386D1729695E00CABF2C /* ReactiveCoreDataApp */, 266 | 1667388D1729695E00CABF2C /* ReactiveCoreDataTests */, 267 | 166738661729695E00CABF2C /* Frameworks */, 268 | 166738651729695E00CABF2C /* Products */, 269 | 166738A017296D0200CABF2C /* ReactiveCocoa.xcodeproj */, 270 | 16BE18C51729786400096F6C /* Expecta.xcodeproj */, 271 | 16BE18D61729787F00096F6C /* Specta.xcodeproj */, 272 | ); 273 | sourceTree = ""; 274 | }; 275 | 166738651729695E00CABF2C /* Products */ = { 276 | isa = PBXGroup; 277 | children = ( 278 | 166738641729695E00CABF2C /* ReactiveCoreData.app */, 279 | 166738871729695E00CABF2C /* ReactiveCoreDataTests.octest */, 280 | ); 281 | name = Products; 282 | sourceTree = ""; 283 | }; 284 | 166738661729695E00CABF2C /* Frameworks */ = { 285 | isa = PBXGroup; 286 | children = ( 287 | 166738671729695E00CABF2C /* Cocoa.framework */, 288 | 166738881729695E00CABF2C /* SenTestingKit.framework */, 289 | 166738691729695E00CABF2C /* Other Frameworks */, 290 | ); 291 | name = Frameworks; 292 | sourceTree = ""; 293 | }; 294 | 166738691729695E00CABF2C /* Other Frameworks */ = { 295 | isa = PBXGroup; 296 | children = ( 297 | 1667386A1729695E00CABF2C /* AppKit.framework */, 298 | 1667386B1729695E00CABF2C /* CoreData.framework */, 299 | 1667386C1729695E00CABF2C /* Foundation.framework */, 300 | ); 301 | name = "Other Frameworks"; 302 | sourceTree = ""; 303 | }; 304 | 1667386D1729695E00CABF2C /* ReactiveCoreDataApp */ = { 305 | isa = PBXGroup; 306 | children = ( 307 | 166738791729695E00CABF2C /* ASWAppDelegate.h */, 308 | 1667387A1729695E00CABF2C /* ASWAppDelegate.m */, 309 | 1667387C1729695E00CABF2C /* MainMenu.xib */, 310 | 1667387F1729695E00CABF2C /* ReactiveCoreData.xcdatamodeld */, 311 | 16CEF48C172ACD7500921072 /* Parent.h */, 312 | 16CEF48D172ACD7500921072 /* Parent.m */, 313 | 1667386E1729695E00CABF2C /* Supporting Files */, 314 | ); 315 | path = ReactiveCoreDataApp; 316 | sourceTree = ""; 317 | }; 318 | 1667386E1729695E00CABF2C /* Supporting Files */ = { 319 | isa = PBXGroup; 320 | children = ( 321 | 1667386F1729695E00CABF2C /* ReactiveCoreData-Info.plist */, 322 | 166738701729695E00CABF2C /* InfoPlist.strings */, 323 | 166738731729695E00CABF2C /* main.m */, 324 | 166738751729695E00CABF2C /* ReactiveCoreData-Prefix.pch */, 325 | 166738761729695E00CABF2C /* Credits.rtf */, 326 | ); 327 | name = "Supporting Files"; 328 | sourceTree = ""; 329 | }; 330 | 1667388D1729695E00CABF2C /* ReactiveCoreDataTests */ = { 331 | isa = PBXGroup; 332 | children = ( 333 | 1667388E1729695E00CABF2C /* Supporting Files */, 334 | 166738BC1729700300CABF2C /* RACManagedObjectFetchSpecs.m */, 335 | ); 336 | path = ReactiveCoreDataTests; 337 | sourceTree = ""; 338 | }; 339 | 1667388E1729695E00CABF2C /* Supporting Files */ = { 340 | isa = PBXGroup; 341 | children = ( 342 | 1667388F1729695E00CABF2C /* ReactiveCoreDataTests-Info.plist */, 343 | 166738901729695E00CABF2C /* InfoPlist.strings */, 344 | ); 345 | name = "Supporting Files"; 346 | sourceTree = ""; 347 | }; 348 | 1667389E17296A8400CABF2C /* ReactiveCoreData */ = { 349 | isa = PBXGroup; 350 | children = ( 351 | 166738B817296FBE00CABF2C /* NSManagedObject+ReactiveCoreData.h */, 352 | 166738B917296FBE00CABF2C /* NSManagedObject+ReactiveCoreData.m */, 353 | 16AF98F717297EBF000E9B8B /* RACSignal+ReactiveCoreData.h */, 354 | 16AF98F817297EBF000E9B8B /* RACSignal+ReactiveCoreData.m */, 355 | 16CEF481172981D700921072 /* NSManagedObjectContext+ReactiveCoreData.h */, 356 | 16CEF482172981D700921072 /* NSManagedObjectContext+ReactiveCoreData.m */, 357 | 1637E1D817328BEA0095B22B /* ReactiveCoreData.h */, 358 | ); 359 | path = ReactiveCoreData; 360 | sourceTree = ""; 361 | }; 362 | 1667389F17296A8E00CABF2C /* External */ = { 363 | isa = PBXGroup; 364 | children = ( 365 | ); 366 | name = External; 367 | sourceTree = ""; 368 | }; 369 | 166738A117296D0200CABF2C /* Products */ = { 370 | isa = PBXGroup; 371 | children = ( 372 | 166738A917296D0200CABF2C /* ReactiveCocoa.framework */, 373 | 166738AB17296D0200CABF2C /* libReactiveCocoa-iOS.a */, 374 | 166738AD17296D0200CABF2C /* ReactiveCocoaTests.octest */, 375 | 80102BC018903EDB002F25AE /* ReactiveCocoaTests-iOS.octest */, 376 | 80102BC218903EDB002F25AE /* ReactiveCocoa-iOS-UIKitTestHost.app */, 377 | 80102BC418903EDB002F25AE /* ReactiveCocoa-iOS-UIKitTestHostTests.octest */, 378 | 80102BC618903EDB002F25AE /* libReactiveCocoa-Mac.a */, 379 | ); 380 | name = Products; 381 | sourceTree = ""; 382 | }; 383 | 16BE18C61729786400096F6C /* Products */ = { 384 | isa = PBXGroup; 385 | children = ( 386 | 16BE18CD1729786400096F6C /* libExpecta.a */, 387 | 16BE18CF1729786400096F6C /* libExpecta-iOS.a */, 388 | 16BE18D11729786400096F6C /* ExpectaTests.octest */, 389 | 16BE18D31729786400096F6C /* Expecta-iOSTests.octest */, 390 | ); 391 | name = Products; 392 | sourceTree = ""; 393 | }; 394 | 16BE18D71729787F00096F6C /* Products */ = { 395 | isa = PBXGroup; 396 | children = ( 397 | 16BE18DE1729787F00096F6C /* libSpecta.a */, 398 | 16BE18E01729787F00096F6C /* libSpecta-iOS.a */, 399 | 16BE18E21729787F00096F6C /* SpectaTests.octest */, 400 | 16BE18E41729787F00096F6C /* Specta-iOSTests.octest */, 401 | ); 402 | name = Products; 403 | sourceTree = ""; 404 | }; 405 | /* End PBXGroup section */ 406 | 407 | /* Begin PBXNativeTarget section */ 408 | 166738631729695E00CABF2C /* ReactiveCoreData */ = { 409 | isa = PBXNativeTarget; 410 | buildConfigurationList = 166738981729695E00CABF2C /* Build configuration list for PBXNativeTarget "ReactiveCoreData" */; 411 | buildPhases = ( 412 | 166738601729695E00CABF2C /* Sources */, 413 | 166738611729695E00CABF2C /* Frameworks */, 414 | 166738621729695E00CABF2C /* Resources */, 415 | 166738AF17296D4300CABF2C /* CopyFiles */, 416 | ); 417 | buildRules = ( 418 | ); 419 | dependencies = ( 420 | 1690735717339D6C007186EE /* PBXTargetDependency */, 421 | ); 422 | name = ReactiveCoreData; 423 | productName = ReactiveCoreData; 424 | productReference = 166738641729695E00CABF2C /* ReactiveCoreData.app */; 425 | productType = "com.apple.product-type.application"; 426 | }; 427 | 166738861729695E00CABF2C /* ReactiveCoreDataTests */ = { 428 | isa = PBXNativeTarget; 429 | buildConfigurationList = 1667389B1729695E00CABF2C /* Build configuration list for PBXNativeTarget "ReactiveCoreDataTests" */; 430 | buildPhases = ( 431 | 166738821729695E00CABF2C /* Sources */, 432 | 166738831729695E00CABF2C /* Frameworks */, 433 | 166738841729695E00CABF2C /* Resources */, 434 | 16CEF498172D25B300921072 /* CopyFiles */, 435 | 166738851729695E00CABF2C /* ShellScript */, 436 | ); 437 | buildRules = ( 438 | ); 439 | dependencies = ( 440 | 16CEF49E172D2A1800921072 /* PBXTargetDependency */, 441 | 16BE18E61729788400096F6C /* PBXTargetDependency */, 442 | 16BE18D51729786B00096F6C /* PBXTargetDependency */, 443 | 1667388C1729695E00CABF2C /* PBXTargetDependency */, 444 | ); 445 | name = ReactiveCoreDataTests; 446 | productName = ReactiveCoreDataTests; 447 | productReference = 166738871729695E00CABF2C /* ReactiveCoreDataTests.octest */; 448 | productType = "com.apple.product-type.bundle"; 449 | }; 450 | /* End PBXNativeTarget section */ 451 | 452 | /* Begin PBXProject section */ 453 | 1667385C1729695E00CABF2C /* Project object */ = { 454 | isa = PBXProject; 455 | attributes = { 456 | CLASSPREFIX = ASW; 457 | LastUpgradeCheck = 0460; 458 | ORGANIZATIONNAME = "Apparent Software"; 459 | }; 460 | buildConfigurationList = 1667385F1729695E00CABF2C /* Build configuration list for PBXProject "ReactiveCoreData" */; 461 | compatibilityVersion = "Xcode 3.2"; 462 | developmentRegion = English; 463 | hasScannedForEncodings = 0; 464 | knownRegions = ( 465 | en, 466 | ); 467 | mainGroup = 1667385B1729695E00CABF2C; 468 | productRefGroup = 166738651729695E00CABF2C /* Products */; 469 | projectDirPath = ""; 470 | projectReferences = ( 471 | { 472 | ProductGroup = 16BE18C61729786400096F6C /* Products */; 473 | ProjectRef = 16BE18C51729786400096F6C /* Expecta.xcodeproj */; 474 | }, 475 | { 476 | ProductGroup = 166738A117296D0200CABF2C /* Products */; 477 | ProjectRef = 166738A017296D0200CABF2C /* ReactiveCocoa.xcodeproj */; 478 | }, 479 | { 480 | ProductGroup = 16BE18D71729787F00096F6C /* Products */; 481 | ProjectRef = 16BE18D61729787F00096F6C /* Specta.xcodeproj */; 482 | }, 483 | ); 484 | projectRoot = ""; 485 | targets = ( 486 | 166738631729695E00CABF2C /* ReactiveCoreData */, 487 | 166738861729695E00CABF2C /* ReactiveCoreDataTests */, 488 | ); 489 | }; 490 | /* End PBXProject section */ 491 | 492 | /* Begin PBXReferenceProxy section */ 493 | 166738A917296D0200CABF2C /* ReactiveCocoa.framework */ = { 494 | isa = PBXReferenceProxy; 495 | fileType = wrapper.framework; 496 | path = ReactiveCocoa.framework; 497 | remoteRef = 166738A817296D0200CABF2C /* PBXContainerItemProxy */; 498 | sourceTree = BUILT_PRODUCTS_DIR; 499 | }; 500 | 166738AB17296D0200CABF2C /* libReactiveCocoa-iOS.a */ = { 501 | isa = PBXReferenceProxy; 502 | fileType = archive.ar; 503 | path = "libReactiveCocoa-iOS.a"; 504 | remoteRef = 166738AA17296D0200CABF2C /* PBXContainerItemProxy */; 505 | sourceTree = BUILT_PRODUCTS_DIR; 506 | }; 507 | 166738AD17296D0200CABF2C /* ReactiveCocoaTests.octest */ = { 508 | isa = PBXReferenceProxy; 509 | fileType = wrapper.cfbundle; 510 | path = ReactiveCocoaTests.octest; 511 | remoteRef = 166738AC17296D0200CABF2C /* PBXContainerItemProxy */; 512 | sourceTree = BUILT_PRODUCTS_DIR; 513 | }; 514 | 16BE18CD1729786400096F6C /* libExpecta.a */ = { 515 | isa = PBXReferenceProxy; 516 | fileType = archive.ar; 517 | path = libExpecta.a; 518 | remoteRef = 16BE18CC1729786400096F6C /* PBXContainerItemProxy */; 519 | sourceTree = BUILT_PRODUCTS_DIR; 520 | }; 521 | 16BE18CF1729786400096F6C /* libExpecta-iOS.a */ = { 522 | isa = PBXReferenceProxy; 523 | fileType = archive.ar; 524 | path = "libExpecta-iOS.a"; 525 | remoteRef = 16BE18CE1729786400096F6C /* PBXContainerItemProxy */; 526 | sourceTree = BUILT_PRODUCTS_DIR; 527 | }; 528 | 16BE18D11729786400096F6C /* ExpectaTests.octest */ = { 529 | isa = PBXReferenceProxy; 530 | fileType = wrapper.cfbundle; 531 | path = ExpectaTests.octest; 532 | remoteRef = 16BE18D01729786400096F6C /* PBXContainerItemProxy */; 533 | sourceTree = BUILT_PRODUCTS_DIR; 534 | }; 535 | 16BE18D31729786400096F6C /* Expecta-iOSTests.octest */ = { 536 | isa = PBXReferenceProxy; 537 | fileType = wrapper.cfbundle; 538 | path = "Expecta-iOSTests.octest"; 539 | remoteRef = 16BE18D21729786400096F6C /* PBXContainerItemProxy */; 540 | sourceTree = BUILT_PRODUCTS_DIR; 541 | }; 542 | 16BE18DE1729787F00096F6C /* libSpecta.a */ = { 543 | isa = PBXReferenceProxy; 544 | fileType = archive.ar; 545 | path = libSpecta.a; 546 | remoteRef = 16BE18DD1729787F00096F6C /* PBXContainerItemProxy */; 547 | sourceTree = BUILT_PRODUCTS_DIR; 548 | }; 549 | 16BE18E01729787F00096F6C /* libSpecta-iOS.a */ = { 550 | isa = PBXReferenceProxy; 551 | fileType = archive.ar; 552 | path = "libSpecta-iOS.a"; 553 | remoteRef = 16BE18DF1729787F00096F6C /* PBXContainerItemProxy */; 554 | sourceTree = BUILT_PRODUCTS_DIR; 555 | }; 556 | 16BE18E21729787F00096F6C /* SpectaTests.octest */ = { 557 | isa = PBXReferenceProxy; 558 | fileType = wrapper.cfbundle; 559 | path = SpectaTests.octest; 560 | remoteRef = 16BE18E11729787F00096F6C /* PBXContainerItemProxy */; 561 | sourceTree = BUILT_PRODUCTS_DIR; 562 | }; 563 | 16BE18E41729787F00096F6C /* Specta-iOSTests.octest */ = { 564 | isa = PBXReferenceProxy; 565 | fileType = wrapper.cfbundle; 566 | path = "Specta-iOSTests.octest"; 567 | remoteRef = 16BE18E31729787F00096F6C /* PBXContainerItemProxy */; 568 | sourceTree = BUILT_PRODUCTS_DIR; 569 | }; 570 | 80102BC018903EDB002F25AE /* ReactiveCocoaTests-iOS.octest */ = { 571 | isa = PBXReferenceProxy; 572 | fileType = wrapper.cfbundle; 573 | path = "ReactiveCocoaTests-iOS.octest"; 574 | remoteRef = 80102BBF18903EDB002F25AE /* PBXContainerItemProxy */; 575 | sourceTree = BUILT_PRODUCTS_DIR; 576 | }; 577 | 80102BC218903EDB002F25AE /* ReactiveCocoa-iOS-UIKitTestHost.app */ = { 578 | isa = PBXReferenceProxy; 579 | fileType = wrapper.application; 580 | path = "ReactiveCocoa-iOS-UIKitTestHost.app"; 581 | remoteRef = 80102BC118903EDB002F25AE /* PBXContainerItemProxy */; 582 | sourceTree = BUILT_PRODUCTS_DIR; 583 | }; 584 | 80102BC418903EDB002F25AE /* ReactiveCocoa-iOS-UIKitTestHostTests.octest */ = { 585 | isa = PBXReferenceProxy; 586 | fileType = wrapper.cfbundle; 587 | path = "ReactiveCocoa-iOS-UIKitTestHostTests.octest"; 588 | remoteRef = 80102BC318903EDB002F25AE /* PBXContainerItemProxy */; 589 | sourceTree = BUILT_PRODUCTS_DIR; 590 | }; 591 | 80102BC618903EDB002F25AE /* libReactiveCocoa-Mac.a */ = { 592 | isa = PBXReferenceProxy; 593 | fileType = archive.ar; 594 | path = "libReactiveCocoa-Mac.a"; 595 | remoteRef = 80102BC518903EDB002F25AE /* PBXContainerItemProxy */; 596 | sourceTree = BUILT_PRODUCTS_DIR; 597 | }; 598 | /* End PBXReferenceProxy section */ 599 | 600 | /* Begin PBXResourcesBuildPhase section */ 601 | 166738621729695E00CABF2C /* Resources */ = { 602 | isa = PBXResourcesBuildPhase; 603 | buildActionMask = 2147483647; 604 | files = ( 605 | 166738721729695E00CABF2C /* InfoPlist.strings in Resources */, 606 | 166738781729695E00CABF2C /* Credits.rtf in Resources */, 607 | 1667387E1729695E00CABF2C /* MainMenu.xib in Resources */, 608 | ); 609 | runOnlyForDeploymentPostprocessing = 0; 610 | }; 611 | 166738841729695E00CABF2C /* Resources */ = { 612 | isa = PBXResourcesBuildPhase; 613 | buildActionMask = 2147483647; 614 | files = ( 615 | 166738921729695E00CABF2C /* InfoPlist.strings in Resources */, 616 | ); 617 | runOnlyForDeploymentPostprocessing = 0; 618 | }; 619 | /* End PBXResourcesBuildPhase section */ 620 | 621 | /* Begin PBXShellScriptBuildPhase section */ 622 | 166738851729695E00CABF2C /* ShellScript */ = { 623 | isa = PBXShellScriptBuildPhase; 624 | buildActionMask = 2147483647; 625 | files = ( 626 | ); 627 | inputPaths = ( 628 | ); 629 | outputPaths = ( 630 | ); 631 | runOnlyForDeploymentPostprocessing = 0; 632 | shellPath = /bin/sh; 633 | shellScript = "# Run the unit tests in this test bundle.\n\"${SYSTEM_DEVELOPER_DIR}/Tools/RunUnitTests\"\n"; 634 | }; 635 | /* End PBXShellScriptBuildPhase section */ 636 | 637 | /* Begin PBXSourcesBuildPhase section */ 638 | 166738601729695E00CABF2C /* Sources */ = { 639 | isa = PBXSourcesBuildPhase; 640 | buildActionMask = 2147483647; 641 | files = ( 642 | 166738741729695E00CABF2C /* main.m in Sources */, 643 | 1667387B1729695E00CABF2C /* ASWAppDelegate.m in Sources */, 644 | 166738811729695E00CABF2C /* ReactiveCoreData.xcdatamodeld in Sources */, 645 | 16CEF48E172ACD7500921072 /* Parent.m in Sources */, 646 | 1690735117339D3E007186EE /* NSManagedObject+ReactiveCoreData.m in Sources */, 647 | 1690735217339D3E007186EE /* RACSignal+ReactiveCoreData.m in Sources */, 648 | 1690735317339D3E007186EE /* NSManagedObjectContext+ReactiveCoreData.m in Sources */, 649 | ); 650 | runOnlyForDeploymentPostprocessing = 0; 651 | }; 652 | 166738821729695E00CABF2C /* Sources */ = { 653 | isa = PBXSourcesBuildPhase; 654 | buildActionMask = 2147483647; 655 | files = ( 656 | 166738BD1729700300CABF2C /* RACManagedObjectFetchSpecs.m in Sources */, 657 | ); 658 | runOnlyForDeploymentPostprocessing = 0; 659 | }; 660 | /* End PBXSourcesBuildPhase section */ 661 | 662 | /* Begin PBXTargetDependency section */ 663 | 1667388C1729695E00CABF2C /* PBXTargetDependency */ = { 664 | isa = PBXTargetDependency; 665 | target = 166738631729695E00CABF2C /* ReactiveCoreData */; 666 | targetProxy = 1667388B1729695E00CABF2C /* PBXContainerItemProxy */; 667 | }; 668 | 1690735717339D6C007186EE /* PBXTargetDependency */ = { 669 | isa = PBXTargetDependency; 670 | name = ReactiveCocoa; 671 | targetProxy = 1690735617339D6C007186EE /* PBXContainerItemProxy */; 672 | }; 673 | 16BE18D51729786B00096F6C /* PBXTargetDependency */ = { 674 | isa = PBXTargetDependency; 675 | name = Expecta; 676 | targetProxy = 16BE18D41729786B00096F6C /* PBXContainerItemProxy */; 677 | }; 678 | 16BE18E61729788400096F6C /* PBXTargetDependency */ = { 679 | isa = PBXTargetDependency; 680 | name = Specta; 681 | targetProxy = 16BE18E51729788400096F6C /* PBXContainerItemProxy */; 682 | }; 683 | 16CEF49E172D2A1800921072 /* PBXTargetDependency */ = { 684 | isa = PBXTargetDependency; 685 | name = ReactiveCocoa; 686 | targetProxy = 16CEF49D172D2A1800921072 /* PBXContainerItemProxy */; 687 | }; 688 | /* End PBXTargetDependency section */ 689 | 690 | /* Begin PBXVariantGroup section */ 691 | 166738701729695E00CABF2C /* InfoPlist.strings */ = { 692 | isa = PBXVariantGroup; 693 | children = ( 694 | 166738711729695E00CABF2C /* en */, 695 | ); 696 | name = InfoPlist.strings; 697 | sourceTree = ""; 698 | }; 699 | 166738761729695E00CABF2C /* Credits.rtf */ = { 700 | isa = PBXVariantGroup; 701 | children = ( 702 | 166738771729695E00CABF2C /* en */, 703 | ); 704 | name = Credits.rtf; 705 | sourceTree = ""; 706 | }; 707 | 1667387C1729695E00CABF2C /* MainMenu.xib */ = { 708 | isa = PBXVariantGroup; 709 | children = ( 710 | 1667387D1729695E00CABF2C /* en */, 711 | ); 712 | name = MainMenu.xib; 713 | sourceTree = ""; 714 | }; 715 | 166738901729695E00CABF2C /* InfoPlist.strings */ = { 716 | isa = PBXVariantGroup; 717 | children = ( 718 | 166738911729695E00CABF2C /* en */, 719 | ); 720 | name = InfoPlist.strings; 721 | sourceTree = ""; 722 | }; 723 | /* End PBXVariantGroup section */ 724 | 725 | /* Begin XCBuildConfiguration section */ 726 | 166738961729695E00CABF2C /* Debug */ = { 727 | isa = XCBuildConfiguration; 728 | buildSettings = { 729 | ALWAYS_SEARCH_USER_PATHS = NO; 730 | ARCHS = "$(ARCHS_STANDARD_64_BIT)"; 731 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 732 | CLANG_CXX_LIBRARY = "libc++"; 733 | CLANG_ENABLE_OBJC_ARC = YES; 734 | CLANG_WARN_CONSTANT_CONVERSION = YES; 735 | CLANG_WARN_EMPTY_BODY = YES; 736 | CLANG_WARN_ENUM_CONVERSION = YES; 737 | CLANG_WARN_INT_CONVERSION = YES; 738 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 739 | COPY_PHASE_STRIP = NO; 740 | GCC_C_LANGUAGE_STANDARD = gnu99; 741 | GCC_DYNAMIC_NO_PIC = NO; 742 | GCC_ENABLE_OBJC_EXCEPTIONS = YES; 743 | GCC_OPTIMIZATION_LEVEL = 0; 744 | GCC_PREPROCESSOR_DEFINITIONS = ( 745 | "DEBUG=1", 746 | "$(inherited)", 747 | ); 748 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 749 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 750 | GCC_WARN_ABOUT_RETURN_TYPE = YES; 751 | GCC_WARN_UNINITIALIZED_AUTOS = YES; 752 | GCC_WARN_UNUSED_VARIABLE = YES; 753 | HEADER_SEARCH_PATHS = ( 754 | "$(BUILD_ROOT)/../IntermediateBuildFilesPath/UninstalledProducts/include", 755 | "$(SRCROOT)/External/ReactiveCocoa/external/specta/src", 756 | "$(SRCROOT)/ReactiveCoreData/", 757 | "$(SRCROOT)/External/ReactiveCocoa/external/expecta/src/**", 758 | "$(inherited)", 759 | ); 760 | MACOSX_DEPLOYMENT_TARGET = 10.7; 761 | ONLY_ACTIVE_ARCH = YES; 762 | SDKROOT = macosx; 763 | }; 764 | name = Debug; 765 | }; 766 | 166738971729695E00CABF2C /* Release */ = { 767 | isa = XCBuildConfiguration; 768 | buildSettings = { 769 | ALWAYS_SEARCH_USER_PATHS = NO; 770 | ARCHS = "$(ARCHS_STANDARD_64_BIT)"; 771 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 772 | CLANG_CXX_LIBRARY = "libc++"; 773 | CLANG_ENABLE_OBJC_ARC = YES; 774 | CLANG_WARN_CONSTANT_CONVERSION = YES; 775 | CLANG_WARN_EMPTY_BODY = YES; 776 | CLANG_WARN_ENUM_CONVERSION = YES; 777 | CLANG_WARN_INT_CONVERSION = YES; 778 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 779 | COPY_PHASE_STRIP = YES; 780 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 781 | GCC_C_LANGUAGE_STANDARD = gnu99; 782 | GCC_ENABLE_OBJC_EXCEPTIONS = YES; 783 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 784 | GCC_WARN_ABOUT_RETURN_TYPE = YES; 785 | GCC_WARN_UNINITIALIZED_AUTOS = YES; 786 | GCC_WARN_UNUSED_VARIABLE = YES; 787 | HEADER_SEARCH_PATHS = ( 788 | "$(BUILD_ROOT)/../IntermediateBuildFilesPath/UninstalledProducts/include", 789 | "$(SRCROOT)/External/ReactiveCocoa/external/specta/src", 790 | "$(SRCROOT)/ReactiveCoreData/", 791 | "$(SRCROOT)/External/ReactiveCocoa/external/expecta/src/**", 792 | "$(inherited)", 793 | ); 794 | MACOSX_DEPLOYMENT_TARGET = 10.7; 795 | SDKROOT = macosx; 796 | }; 797 | name = Release; 798 | }; 799 | 166738991729695E00CABF2C /* Debug */ = { 800 | isa = XCBuildConfiguration; 801 | buildSettings = { 802 | COMBINE_HIDPI_IMAGES = YES; 803 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 804 | GCC_PREFIX_HEADER = "ReactiveCoreDataApp/ReactiveCoreData-Prefix.pch"; 805 | INFOPLIST_FILE = "ReactiveCoreDataApp/ReactiveCoreData-Info.plist"; 806 | MACOSX_DEPLOYMENT_TARGET = 10.7; 807 | PRODUCT_NAME = "$(TARGET_NAME)"; 808 | WRAPPER_EXTENSION = app; 809 | }; 810 | name = Debug; 811 | }; 812 | 1667389A1729695E00CABF2C /* Release */ = { 813 | isa = XCBuildConfiguration; 814 | buildSettings = { 815 | COMBINE_HIDPI_IMAGES = YES; 816 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 817 | GCC_PREFIX_HEADER = "ReactiveCoreDataApp/ReactiveCoreData-Prefix.pch"; 818 | INFOPLIST_FILE = "ReactiveCoreDataApp/ReactiveCoreData-Info.plist"; 819 | MACOSX_DEPLOYMENT_TARGET = 10.7; 820 | PRODUCT_NAME = "$(TARGET_NAME)"; 821 | WRAPPER_EXTENSION = app; 822 | }; 823 | name = Release; 824 | }; 825 | 1667389C1729695E00CABF2C /* Debug */ = { 826 | isa = XCBuildConfiguration; 827 | buildSettings = { 828 | BUNDLE_LOADER = "$(BUILT_PRODUCTS_DIR)/ReactiveCoreData.app/Contents/MacOS/ReactiveCoreData"; 829 | COMBINE_HIDPI_IMAGES = YES; 830 | FRAMEWORK_SEARCH_PATHS = "\"$(DEVELOPER_LIBRARY_DIR)/Frameworks\""; 831 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 832 | GCC_PREFIX_HEADER = "ReactiveCoreDataApp/ReactiveCoreData-Prefix.pch"; 833 | INFOPLIST_FILE = "ReactiveCoreDataTests/ReactiveCoreDataTests-Info.plist"; 834 | OTHER_LDFLAGS = "-ObjC"; 835 | PRODUCT_NAME = "$(TARGET_NAME)"; 836 | TEST_HOST = "$(BUNDLE_LOADER)"; 837 | WRAPPER_EXTENSION = octest; 838 | }; 839 | name = Debug; 840 | }; 841 | 1667389D1729695E00CABF2C /* Release */ = { 842 | isa = XCBuildConfiguration; 843 | buildSettings = { 844 | BUNDLE_LOADER = "$(BUILT_PRODUCTS_DIR)/ReactiveCoreData.app/Contents/MacOS/ReactiveCoreData"; 845 | COMBINE_HIDPI_IMAGES = YES; 846 | FRAMEWORK_SEARCH_PATHS = "\"$(DEVELOPER_LIBRARY_DIR)/Frameworks\""; 847 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 848 | GCC_PREFIX_HEADER = "ReactiveCoreDataApp/ReactiveCoreData-Prefix.pch"; 849 | INFOPLIST_FILE = "ReactiveCoreDataTests/ReactiveCoreDataTests-Info.plist"; 850 | OTHER_LDFLAGS = "-ObjC"; 851 | PRODUCT_NAME = "$(TARGET_NAME)"; 852 | TEST_HOST = "$(BUNDLE_LOADER)"; 853 | WRAPPER_EXTENSION = octest; 854 | }; 855 | name = Release; 856 | }; 857 | /* End XCBuildConfiguration section */ 858 | 859 | /* Begin XCConfigurationList section */ 860 | 1667385F1729695E00CABF2C /* Build configuration list for PBXProject "ReactiveCoreData" */ = { 861 | isa = XCConfigurationList; 862 | buildConfigurations = ( 863 | 166738961729695E00CABF2C /* Debug */, 864 | 166738971729695E00CABF2C /* Release */, 865 | ); 866 | defaultConfigurationIsVisible = 0; 867 | defaultConfigurationName = Release; 868 | }; 869 | 166738981729695E00CABF2C /* Build configuration list for PBXNativeTarget "ReactiveCoreData" */ = { 870 | isa = XCConfigurationList; 871 | buildConfigurations = ( 872 | 166738991729695E00CABF2C /* Debug */, 873 | 1667389A1729695E00CABF2C /* Release */, 874 | ); 875 | defaultConfigurationIsVisible = 0; 876 | defaultConfigurationName = Release; 877 | }; 878 | 1667389B1729695E00CABF2C /* Build configuration list for PBXNativeTarget "ReactiveCoreDataTests" */ = { 879 | isa = XCConfigurationList; 880 | buildConfigurations = ( 881 | 1667389C1729695E00CABF2C /* Debug */, 882 | 1667389D1729695E00CABF2C /* Release */, 883 | ); 884 | defaultConfigurationIsVisible = 0; 885 | defaultConfigurationName = Release; 886 | }; 887 | /* End XCConfigurationList section */ 888 | 889 | /* Begin XCVersionGroup section */ 890 | 1667387F1729695E00CABF2C /* ReactiveCoreData.xcdatamodeld */ = { 891 | isa = XCVersionGroup; 892 | children = ( 893 | 166738801729695E00CABF2C /* ReactiveCoreData.xcdatamodel */, 894 | ); 895 | currentVersion = 166738801729695E00CABF2C /* ReactiveCoreData.xcdatamodel */; 896 | path = ReactiveCoreData.xcdatamodeld; 897 | sourceTree = ""; 898 | versionGroupType = wrapper.xcdatamodel; 899 | }; 900 | /* End XCVersionGroup section */ 901 | }; 902 | rootObject = 1667385C1729695E00CABF2C /* Project object */; 903 | } 904 | -------------------------------------------------------------------------------- /ReactiveCoreData/NSManagedObject+ReactiveCoreData.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSManagedObject+ReactiveCoreData.h 3 | // ReactiveCoreData 4 | // 5 | // Created by Jacob Gorban on 25/04/2013. 6 | // Copyright (c) 2013 Apparent Software. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @class RACSignal; 12 | 13 | @interface NSManagedObject (ReactiveCoreData) 14 | 15 | // Creates a signal that sends an NSFetchRequest for specified entity 16 | + (RACSignal *)findAll; 17 | 18 | // Creates a signal that sends a NSFetchRequest with fetchLimit of 1 19 | // 20 | // In the end, fetch will return only one object instead of arrays for such requests 21 | + (RACSignal *)findOne; 22 | 23 | // returns Entity name string 24 | // 25 | // By default returns class name string (which works well for XCode generated subclasses) 26 | // mogenerator also defines such a method in its private subclass interface, so it'll override this one 27 | + (NSString*)entityName; 28 | 29 | // Inserts a new object in the current context and returns it 30 | + (instancetype)insert; 31 | 32 | // Convenience method that for faster insertion with values 33 | // 34 | // Inserts a new object and passes it to the config block 35 | // Return the newly configured object 36 | + (instancetype)insert:(void (^)(id))configBlock; 37 | @end 38 | -------------------------------------------------------------------------------- /ReactiveCoreData/NSManagedObject+ReactiveCoreData.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSManagedObject+ReactiveCoreData.m 3 | // ReactiveCoreData 4 | // 5 | // Created by Jacob Gorban on 25/04/2013. 6 | // Copyright (c) 2013 Apparent Software. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "NSManagedObject+ReactiveCoreData.h" 11 | #import "NSManagedObjectContext+ReactiveCoreData.h" 12 | #import "RACSignal+ReactiveCoreData.h" 13 | 14 | @implementation NSManagedObject (ReactiveCoreData) 15 | 16 | + (RACSignal *)findAll; 17 | { 18 | return [RACSignal createSignal:^RACDisposable *(id subscriber) { 19 | NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:self.entityName]; 20 | [subscriber sendNext:fetchRequest]; 21 | [subscriber sendCompleted]; 22 | return nil; 23 | }]; 24 | } 25 | 26 | + (RACSignal *)findOne; 27 | { 28 | return [[self findAll] 29 | map:^id(NSFetchRequest *req) { 30 | req.fetchLimit = 1; 31 | return req; 32 | }]; 33 | } 34 | 35 | 36 | + (NSString *)entityName; 37 | { 38 | return NSStringFromClass(self); 39 | } 40 | 41 | + (instancetype)insert; 42 | { 43 | return [NSEntityDescription insertNewObjectForEntityForName:[self entityName] inManagedObjectContext:[NSManagedObjectContext currentContext]]; 44 | } 45 | 46 | + (instancetype)insert:(void (^)(id obj))configBlock; 47 | { 48 | id object = [self insert]; 49 | configBlock(object); 50 | return object; 51 | } 52 | 53 | @end 54 | -------------------------------------------------------------------------------- /ReactiveCoreData/NSManagedObjectContext+ReactiveCoreData.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSManagedObjectContext+ReactiveCoreData.h 3 | // ReactiveCoreData 4 | // 5 | // Created by Jacob Gorban on 25/04/2013. 6 | // Copyright (c) 2013 Apparent Software. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @class RACSignal; 12 | 13 | static NSString const *kRCDCurrentManagedObjectContext; 14 | static NSString const *kRCDMainManagedObjectContext; 15 | 16 | @interface NSManagedObjectContext (ReactiveCoreData) 17 | 18 | // sends the NSManagedObjectContextDidSaveNotification notification when the context is merged into 19 | // This is for the main context, sends on the main thread 20 | @property (readonly, nonatomic) RACSignal *rcd_merged; 21 | 22 | // sends the NSManagedObjectContextDidSaveNotification notification when the context is saved 23 | @property (readonly, nonatomic) RACSignal *rcd_saved; 24 | 25 | // Returns a signal that sends result of executing a fetch request (or sends error) 26 | - (RACSignal *)executeRequest:(NSFetchRequest *)request; 27 | 28 | // Returns a signal that sends result of executing a count of the fetch request (or sends error) 29 | - (RACSignal *)countRequest:(NSFetchRequest *)request; 30 | 31 | // Creates a new context based on the current context 32 | + (NSManagedObjectContext *)context; 33 | 34 | // Set self as current context and starts a signal 35 | // 36 | // Passes self as signal's value 37 | // This is needed when you have contexts per-document 38 | // and need to perform operations in a specific context 39 | // Probably only works well on the main thread 40 | - (RACSignal *)perform; 41 | 42 | // Sets `moc` as the current context for the main scheduler and sets it as current context 43 | // This is mostly needed for shoebox-type apps 44 | + (void)setMainContext:(NSManagedObjectContext *)moc; 45 | 46 | // Returns the context registered with current RACScheduler 47 | + (NSManagedObjectContext *)currentContext; 48 | 49 | // Attaches self to the current scheduler 50 | - (void)attachToCurrentScheduler; 51 | 52 | // returns the context that self merges into 53 | - (NSManagedObjectContext *)mainContext; 54 | 55 | // Convenience method for shoebox contexts to start perform in the background right away 56 | - (RACSignal *)performInBackground; 57 | 58 | // Actually creates a new child context for the passed main context 59 | + (NSManagedObjectContext *)contextWithMainContext:(NSManagedObjectContext *)mainContext; 60 | @end 61 | -------------------------------------------------------------------------------- /ReactiveCoreData/NSManagedObjectContext+ReactiveCoreData.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSManagedObjectContext+ReactiveCoreData.m 3 | // ReactiveCoreData 4 | // 5 | // Created by Jacob Gorban on 25/04/2013. 6 | // Copyright (c) 2013 Apparent Software. All rights reserved. 7 | // 8 | 9 | #import 10 | #import 11 | #import 12 | #import "NSManagedObjectContext+ReactiveCoreData.h" 13 | #import "RACSignal+ReactiveCoreData.h" 14 | 15 | static NSString const *kRCDCurrentManagedObjectContext = @"kRCDCurrentManagedObjectContext"; 16 | static NSString const *kRCDMainManagedObjectContext = @"kRCDMainManagedObjectContext"; 17 | 18 | @implementation NSManagedObjectContext (ReactiveCoreData) 19 | 20 | - (RACSignal *)executeRequest:(NSFetchRequest *)request 21 | { 22 | return [RACSignal createSignal:^RACDisposable *(id subscriber) { 23 | NSError *error = nil; 24 | NSArray *result = [self executeFetchRequest:request error:&error]; 25 | if (error) { 26 | [subscriber sendError:error]; 27 | return nil; 28 | } 29 | [subscriber sendNext:result]; 30 | [subscriber sendCompleted]; 31 | return nil; 32 | }]; 33 | } 34 | 35 | - (RACSignal *)countRequest:(NSFetchRequest *)request 36 | { 37 | return [RACSignal createSignal:^RACDisposable *(id subscriber) { 38 | NSError *error = nil; 39 | NSUInteger count = [self countForFetchRequest:request error:&error]; 40 | if (error) { 41 | [subscriber sendError:error]; 42 | return nil; 43 | } 44 | [subscriber sendNext:@(count)]; 45 | [subscriber sendCompleted]; 46 | return nil; 47 | }]; 48 | } 49 | 50 | + (NSManagedObjectContext *)context; 51 | { 52 | return [self contextWithMainContext:[self currentContext]]; 53 | } 54 | 55 | + (NSManagedObjectContext *)mainMoc; 56 | { 57 | return [self contextForScheduler:[RACScheduler mainThreadScheduler]]; 58 | } 59 | 60 | - (void)rcd_mergeChanges:(NSNotification *)note; 61 | { 62 | NSManagedObjectContext *mainContext = [self mainContext]; 63 | NSAssert(mainContext, @"no main context"); 64 | [mainContext performSelector:@selector(mergeChangesFromContextDidSaveNotification:) onThread:[NSThread mainThread] withObject:note waitUntilDone:YES]; 65 | [((RACSubject *) mainContext.rcd_merged) sendNext:note]; 66 | } 67 | 68 | + (void)setCurrentContext:(NSManagedObjectContext *)moc; 69 | { 70 | [moc attachToCurrentScheduler]; 71 | } 72 | 73 | + (void)setMainContext:(NSManagedObjectContext *)moc; 74 | { 75 | RACScheduler *scheduler = [RACScheduler mainThreadScheduler]; 76 | objc_setAssociatedObject(scheduler, (__bridge void *)kRCDCurrentManagedObjectContext, self, OBJC_ASSOCIATION_RETAIN_NONATOMIC); 77 | [self setCurrentContext:moc]; 78 | } 79 | 80 | + (NSManagedObjectContext *)currentContext; 81 | { 82 | return [self contextForScheduler:[RACScheduler currentScheduler]]; 83 | } 84 | 85 | + (NSManagedObjectContext *)contextForScheduler:(RACScheduler *)scheduler; 86 | { 87 | NSManagedObjectContext *schedulerContext = objc_getAssociatedObject(scheduler, (__bridge void *)kRCDCurrentManagedObjectContext); 88 | return schedulerContext; 89 | } 90 | 91 | - (RACSignal *)rcd_merged; 92 | { 93 | RACSubject *merged = objc_getAssociatedObject(self, _cmd); 94 | if (!merged) { 95 | merged = [RACSubject subject]; 96 | objc_setAssociatedObject(self, _cmd, merged, OBJC_ASSOCIATION_RETAIN); 97 | [self.rac_deallocDisposable addDisposable:[RACDisposable disposableWithBlock:^{ 98 | [merged sendCompleted]; 99 | }]]; 100 | } 101 | return merged; 102 | } 103 | 104 | - (RACSignal *)rcd_saved; 105 | { 106 | RACSignal *saved = objc_getAssociatedObject(self, _cmd); 107 | if (!saved) { 108 | saved = [[NSNotificationCenter defaultCenter] rac_addObserverForName:NSManagedObjectContextDidSaveNotification object:self]; 109 | objc_setAssociatedObject(self, _cmd, saved, OBJC_ASSOCIATION_RETAIN); 110 | } 111 | return saved; 112 | } 113 | 114 | - (NSManagedObjectContext *)mainContext 115 | { 116 | return self.userInfo[kRCDMainManagedObjectContext]; 117 | } 118 | 119 | + (NSManagedObjectContext *)contextWithMainContext:(NSManagedObjectContext *)mainContext; 120 | { 121 | NSParameterAssert(mainContext); 122 | NSManagedObjectContext *moc = [[self alloc] initWithConcurrencyType:NSConfinementConcurrencyType]; 123 | moc.userInfo[kRCDMainManagedObjectContext] = mainContext; 124 | moc.persistentStoreCoordinator = mainContext.persistentStoreCoordinator; 125 | 126 | [NSNotificationCenter.defaultCenter addObserver:moc selector:@selector(rcd_mergeChanges:) name:NSManagedObjectContextDidSaveNotification object:moc]; 127 | [moc.rac_deallocDisposable addDisposable:[RACDisposable disposableWithBlock:^{ 128 | [NSNotificationCenter.defaultCenter removeObserver:moc name:NSManagedObjectContextDidSaveNotification object:moc]; 129 | }]]; 130 | 131 | return moc; 132 | } 133 | 134 | 135 | - (RACSignal *)perform; 136 | { 137 | NSManagedObjectContext *oldContext = [NSManagedObjectContext currentContext]; 138 | [NSManagedObjectContext setCurrentContext:self]; 139 | return [RACSignal createSignal:^RACDisposable *(id subscriber) { 140 | [subscriber sendNext:self]; 141 | [subscriber sendCompleted]; 142 | [NSManagedObjectContext setCurrentContext:oldContext]; 143 | return nil; 144 | }]; 145 | } 146 | 147 | - (void)attachToCurrentScheduler; 148 | { 149 | RACScheduler *scheduler = [RACScheduler currentScheduler]; 150 | objc_setAssociatedObject(scheduler, (__bridge void *)kRCDCurrentManagedObjectContext, self, OBJC_ASSOCIATION_RETAIN_NONATOMIC); 151 | } 152 | 153 | - (RACSignal *)performInBackground; 154 | { 155 | return [[self perform] performInBackgroundContext]; 156 | } 157 | @end 158 | -------------------------------------------------------------------------------- /ReactiveCoreData/RACSignal+ReactiveCoreData.h: -------------------------------------------------------------------------------- 1 | // 2 | // RACSignal+ReactiveCoreData.h 3 | // ReactiveCoreData 4 | // 5 | // Created by Jacob Gorban on 25/04/2013. 6 | // Copyright (c) 2013 Apparent Software. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface RACSignal (ReactiveCoreData) 12 | 13 | // Execute the NSFetchRequest that's sent in next, in specified context 14 | // 15 | // If the fetch request fetchLimit is set to 1, the signal will carry the object itself 16 | // if fetchLimit != 1, then the signal will carry the resulting array 17 | - (RACSignal *)fetchInMOC:(NSManagedObjectContext *)moc; 18 | 19 | // Returns signal with the count of results that would've returned in `fetchInMOC:` 20 | - (RACSignal *)countInMOC:(NSManagedObjectContext *)moc; 21 | 22 | // Returns a signal that contains the NSArray of the fetch result in current NSManagedObjectContext 23 | - (RACSignal *)fetch; 24 | 25 | // Returns a signal that contains the count of results of the fetch request in current NSManagedObjectContext 26 | - (RACSignal *)count; 27 | 28 | // Modifies NSFetchRequest to set predicate 29 | - (RACSignal *)where:(id)predicateOrSignal; 30 | 31 | // Returns a signal with NSFetchRequest's predicate modified according to format and its arguments 32 | // 33 | // The format is passed to NSPredicate as is 34 | // The arguments can be either signals or objects that can be returned in signals 35 | // Any new value in any of the argument signals will result in update of the fetch request 36 | // and possible execution of the request, if there's a `fetch` later. 37 | // This brings the predicates into the reactive world 38 | - (RACSignal *)where:(NSString *)format args:(NSArray *)args; 39 | 40 | // A convenience method for a common predicate case 41 | // 42 | // Create a "%K == %@" predicate with key and value as arguments 43 | - (RACSignal *)where:(id)key equals:(id)value; 44 | 45 | // A convenience method for a common predicate case 46 | // 47 | // Creates a "%K CONTAINS[options] %@" predicate with key and value as arguments and adds it to the fetch request 48 | // The key may be a signal 49 | // If the `contains` parameter value is an empty string, it won't add the predicate, instead passing the fetch request as is 50 | // This is useful when the using it to filter text from the search field, which can be empty 51 | // `options` parameter is an optional string like `@"cd"` that can be added after CONTAINS inside brackets. 52 | // For example, passing @"cd" for `options` will result in a CONTAINS[cd] predicate 53 | - (RACSignal *)where:(id)key contains:(id)valueOrSignal options:(NSString *)optionsOrNil; 54 | 55 | // Modifies the NSFetchRequest to set passed-in limit 56 | - (RACSignal *)limit:(id)limitOrSignal; 57 | 58 | // Modifies NSFetchRequest to set sortDescriptor 59 | // 60 | // The `sortOrSignal` parameter may be one of the following: 61 | // - An NSSortDescriptor 62 | // - An NSArray of NSSortDescriptors 63 | // - An NSString of the key to be sorted in ascending order 64 | // - An NSString of the key, prefixed by a minus (@"-key") to sort key in descending order 65 | // - A RACSignal that sends any of the above values 66 | - (RACSignal *)sortBy:(id)sortOrSignal; 67 | 68 | // Modifies the NSFetchRequest. Sets resultType to NSManagedObjectIDResultType 69 | - (RACSignal *)IDResultType; 70 | 71 | // Saves current NSManagedObjectContext and waits for it to merge 72 | // 73 | // Send an error if couldn't save 74 | // Passes next from previous subscriber on to the next one 75 | - (RACSignal *)saveContext; 76 | 77 | // Sets context as current context and return self 78 | // 79 | // Use it to start inserting document-based Core Data operations on the passed context 80 | - (RACSignal *)performInContext:(NSManagedObjectContext *)context; 81 | 82 | // Creates a new background scheduler and context. 83 | // 84 | // Sets the context as current for this scheduler and further chain runs on this scheduler 85 | - (RACSignal *)performInBackgroundContext; 86 | 87 | // Creates a new background scheduler and context and executes the passed-in block 88 | // 89 | // Sets the context as current for this scheduler and further chain runs on this scheduler 90 | - (RACSignal *)performInBackgroundContext:(void (^)(NSManagedObjectContext *))block; 91 | 92 | // Will rerun fetch when triggerSignal sends any next value 93 | - (RACSignal *)fetchWithTrigger:(RACSignal *)triggerSignal; 94 | 95 | // Will return a signal with a fetch request for the given entity name 96 | // 97 | // It disregards the value in the signal that it follows 98 | - (RACSignal *)findAll:(NSString *)entityName; 99 | 100 | // Similar to findAll but also sets fetchLimit to 1 101 | - (RACSignal *)findOne:(NSString *)entityName; 102 | 103 | // converts a signal of NSManagedObjectID array to array of these objects in current context 104 | - (RACSignal *)objectIDsToObjects; 105 | 106 | @end 107 | -------------------------------------------------------------------------------- /ReactiveCoreData/RACSignal+ReactiveCoreData.m: -------------------------------------------------------------------------------- 1 | // 2 | // RACSignal+ReactiveCoreData.m 3 | // ReactiveCoreData 4 | // 5 | // Created by Jacob Gorban on 25/04/2013. 6 | // Copyright (c) 2013 Apparent Software. All rights reserved. 7 | // 8 | 9 | #import "RACSignal+ReactiveCoreData.h" 10 | #import "NSManagedObjectContext+ReactiveCoreData.h" 11 | 12 | @implementation RACSignal (ReactiveCoreData) 13 | 14 | - (RACSignal *)fetchInMOC:(NSManagedObjectContext *)moc; 15 | { 16 | return [[self flattenMap:^RACStream *(NSFetchRequest *req) { 17 | if (req.fetchLimit == 1) { 18 | return [[moc executeRequest:req] map:^id(id value) { 19 | return [value lastObject]; 20 | }]; 21 | } 22 | else { 23 | return [moc executeRequest:req]; 24 | } 25 | }] setNameWithFormat:@"[%@] -fetchInMOC:%@", self.name, moc]; 26 | } 27 | 28 | - (RACSignal *)countInMOC:(NSManagedObjectContext *)moc; 29 | { 30 | return [[self flattenMap:^RACStream *(NSFetchRequest *req) { 31 | return [moc countRequest:req]; 32 | }] setNameWithFormat:@"[%@] -countInMOC:%@", self.name, moc]; 33 | } 34 | 35 | - (RACSignal *)fetch; 36 | { 37 | 38 | NSManagedObjectContext *currentMoc = [NSManagedObjectContext currentContext]; 39 | return [self fetchInMOC:currentMoc]; 40 | } 41 | 42 | - (RACSignal *)count; 43 | { 44 | return [self countInMOC:[NSManagedObjectContext currentContext]]; 45 | } 46 | 47 | #pragma mark - Operations modifying NSFetchRequest 48 | 49 | - (RACSignal *)where:(id)predicateOrSignal; 50 | { 51 | RACSignal *predicateSignal = [self rcd_convertToSignal:predicateOrSignal]; 52 | return [[[self combineLatestWith:predicateSignal] 53 | reduceEach:^(NSFetchRequest *request, NSPredicate *predicate) { 54 | request.predicate = predicate; 55 | return request; 56 | }] setNameWithFormat:@"[%@] -where:%@", self.name, predicateOrSignal]; 57 | } 58 | 59 | - (RACSignal *)where:(id)key equals:(id)value; 60 | { 61 | return [self where:@"%K == %@" args:@[key, value]]; 62 | } 63 | 64 | - (RACSignal *)where:(NSString *)format args:(NSArray *)args; 65 | { 66 | NSArray *signals = [self rcd_convertToSignals:args]; 67 | return [[[self combineLatestWith:[RACSignal combineLatest:signals]] 68 | reduceEach:^(NSFetchRequest *req, RACTuple *arguments) { 69 | NSPredicate *predicate = [NSPredicate predicateWithFormat:format argumentArray:[arguments allObjects]]; 70 | req.predicate = predicate; 71 | return req; 72 | }] setNameWithFormat:@"[%@] -where:%@ args:%@", self.name, format, args]; 73 | } 74 | 75 | - (RACSignal *)where:(id)key contains:(id)valueOrSignal options:(NSString *)optionsOrNil; 76 | { 77 | NSParameterAssert(valueOrSignal); 78 | NSParameterAssert(key); 79 | return [[self rcd_convertToSignal:valueOrSignal] 80 | flattenMap:^(NSString *filter) { 81 | if ([filter length] > 0) { 82 | NSString *whereClause; 83 | if (optionsOrNil) { 84 | whereClause = [NSString stringWithFormat:@"%%K CONTAINS[%@] %%@", optionsOrNil]; 85 | } 86 | else { 87 | whereClause = @"%K CONTAINS %@"; 88 | } 89 | return [self where:whereClause args:@[key, filter]]; 90 | } 91 | else 92 | return self; 93 | }]; 94 | } 95 | 96 | - (RACSignal *)limit:(id)limitOrSignal; 97 | { 98 | RACSignal *limitSignal = [self rcd_convertToSignal:limitOrSignal]; 99 | return [[[self combineLatestWith:limitSignal] 100 | reduceEach:^(NSFetchRequest *req, NSNumber *limit) { 101 | req.fetchLimit = limit.unsignedIntegerValue; 102 | return req; 103 | }] setNameWithFormat:@"[%@] -limit:%@", self.name, limitOrSignal ]; 104 | } 105 | 106 | - (RACSignal *)IDResultType; 107 | { 108 | return [[self map:^id(NSFetchRequest *fetchRequest) { 109 | fetchRequest.resultType = NSManagedObjectIDResultType; 110 | return fetchRequest; 111 | }] setNameWithFormat:@"[%@] -IDResultType", self.name]; 112 | } 113 | 114 | - (RACSignal *)sortBy:(id)sortOrSignal; 115 | { 116 | RACSignal *sortSignal = [self rcd_convertToSignal:sortOrSignal]; 117 | return [[[self combineLatestWith:sortSignal] 118 | reduceEach:^(NSFetchRequest *fetchRequest, id sortValue) { 119 | if ([sortValue isKindOfClass:[NSSortDescriptor class]]) { 120 | sortValue = @[sortValue]; 121 | } 122 | else if ([sortValue isKindOfClass:[NSString class]]) { 123 | NSAssert([sortValue length] > 0, @"Key to sort by can't be empty"); 124 | BOOL ascending = ([sortValue characterAtIndex:0] != '-'); 125 | NSString *key = ascending ? sortValue : [sortValue substringFromIndex:1]; 126 | sortValue = @[ [NSSortDescriptor sortDescriptorWithKey:key ascending:ascending] ]; 127 | } 128 | fetchRequest.sortDescriptors = sortValue; 129 | return fetchRequest; 130 | }] setNameWithFormat:@"[%@] -sortBy:%@", self.name, sortOrSignal]; 131 | } 132 | 133 | - (RACSignal *)saveContext; 134 | { 135 | return [[RACSignal createSignal:^RACDisposable *(id subscriber) { 136 | return [self 137 | subscribeNext:^(id x) { 138 | NSError *error = nil; 139 | BOOL success = [[NSManagedObjectContext currentContext] save:&error]; 140 | if (!success) { 141 | [subscriber sendError:error]; 142 | } 143 | else { 144 | [subscriber sendNext:x]; 145 | } 146 | } 147 | error:^(NSError *error) { 148 | [subscriber sendError:error]; 149 | } 150 | completed:^{ 151 | [subscriber sendCompleted]; 152 | }]; 153 | }] setNameWithFormat:@"[%@] -saveContext", self.name]; 154 | } 155 | 156 | - (RACSignal *)performInContext:(NSManagedObjectContext *)context 157 | { 158 | [context attachToCurrentScheduler]; 159 | return self; 160 | } 161 | 162 | - (RACSignal *)performInBackgroundContext; 163 | { 164 | RACScheduler *scheduler = [RACScheduler schedulerWithPriority:RACSchedulerPriorityDefault name:@"com.ReactiveCoreData.background"]; 165 | NSManagedObjectContext *currentContext = [NSManagedObjectContext currentContext]; 166 | 167 | return [[[self deliverOn:scheduler] doNext:^(id x) { 168 | NSManagedObjectContext *childContext = [NSManagedObjectContext currentContext]; 169 | if (!childContext) { 170 | childContext = [NSManagedObjectContext contextWithMainContext:currentContext]; 171 | } 172 | [childContext attachToCurrentScheduler]; 173 | }] setNameWithFormat:@"[%@] -performInBackgroundContext", self.name]; 174 | } 175 | 176 | - (RACSignal *)performInBackgroundContext:(void(^)(NSManagedObjectContext *))block; 177 | { 178 | return [[self performInBackgroundContext] 179 | doNext:^(id x) { 180 | block([NSManagedObjectContext currentContext]); 181 | }]; 182 | } 183 | 184 | - (RACSignal *)fetchWithTrigger:(RACSignal *)triggerSignal; 185 | { 186 | return [[[self combineLatestWith:triggerSignal] 187 | map:^(RACTuple *tuple) { 188 | return [tuple first]; 189 | }] 190 | fetch]; 191 | } 192 | 193 | #pragma mark - Operations starting a new NSFetchRequest signal 194 | - (RACSignal *)findAll:(NSString *)entityName; 195 | { 196 | NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:entityName]; 197 | return [RACSignal return:fetchRequest]; 198 | } 199 | 200 | - (RACSignal *)findOne:(NSString *)entityName; 201 | { 202 | NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:entityName]; 203 | fetchRequest.fetchLimit = 1; 204 | return [RACSignal return:fetchRequest]; 205 | } 206 | 207 | - (RACSignal *)objectIDsToObjects; 208 | { 209 | return [self map:^id(NSArray *objectIDs) { 210 | NSManagedObjectContext *context = [NSManagedObjectContext currentContext]; 211 | NSMutableArray *objects = [NSMutableArray arrayWithCapacity:[objectIDs count]]; 212 | for (NSManagedObjectID *objectID in objectIDs) { 213 | [objects addObject:[context objectWithID:objectID]]; 214 | } 215 | return objects; 216 | }]; 217 | } 218 | 219 | #pragma mark - Private 220 | 221 | - (RACSignal *)rcd_convertToSignal:(id)valueOrSignal; 222 | { 223 | if ([valueOrSignal isKindOfClass:[RACStream class]]) 224 | return valueOrSignal; 225 | return [RACSignal return:valueOrSignal]; 226 | } 227 | 228 | - (NSArray *)rcd_convertToSignals:(NSArray *)args; 229 | { 230 | NSMutableArray *signals = [NSMutableArray arrayWithCapacity:[args count]]; 231 | for (id arg in args) { 232 | [signals addObject:[self rcd_convertToSignal:arg]]; 233 | } 234 | return [signals copy]; 235 | } 236 | 237 | @end 238 | -------------------------------------------------------------------------------- /ReactiveCoreData/ReactiveCoreData.h: -------------------------------------------------------------------------------- 1 | // 2 | // ReactiveCoreData.h 3 | // ReactiveCoreData 4 | // 5 | // Created by Jacob Gorban on 02/05/2013. 6 | // Copyright (c) 2013 Apparent Software. All rights reserved. 7 | // 8 | 9 | #ifndef ReactiveCoreData_ReactiveCoreData_h 10 | #define ReactiveCoreData_ReactiveCoreData_h 11 | 12 | #import "NSManagedObject+ReactiveCoreData.h" 13 | #import "RACSignal+ReactiveCoreData.h" 14 | #import "NSManagedObjectContext+ReactiveCoreData.h" 15 | 16 | #endif 17 | -------------------------------------------------------------------------------- /ReactiveCoreDataApp/ASWAppDelegate.h: -------------------------------------------------------------------------------- 1 | // 2 | // ASWAppDelegate.h 3 | // ReactiveCoreData 4 | // 5 | // Created by Jacob Gorban on 25/04/2013. 6 | // Copyright (c) 2013 Apparent Software. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface ASWAppDelegate : NSObject 12 | 13 | @property (assign) IBOutlet NSWindow *window; 14 | 15 | @property (readonly, strong, nonatomic) NSPersistentStoreCoordinator *persistentStoreCoordinator; 16 | @property (readonly, strong, nonatomic) NSManagedObjectModel *managedObjectModel; 17 | @property (readonly, strong, nonatomic) NSManagedObjectContext *managedObjectContext; 18 | 19 | - (IBAction)saveAction:(id)sender; 20 | 21 | @end 22 | -------------------------------------------------------------------------------- /ReactiveCoreDataApp/ASWAppDelegate.m: -------------------------------------------------------------------------------- 1 | // 2 | // ASWAppDelegate.m 3 | // ReactiveCoreData 4 | // 5 | // Created by Jacob Gorban on 25/04/2013. 6 | // Copyright (c) 2013 Apparent Software. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "ASWAppDelegate.h" 11 | #import "ReactiveCoreData.h" 12 | #import "Parent.h" 13 | 14 | @interface ASWAppDelegate () 15 | @property (weak) IBOutlet NSButton *addButton; 16 | @property (weak) IBOutlet NSButton *removeButton; 17 | @property (weak) IBOutlet NSSearchField *searchField; 18 | @property (weak) IBOutlet NSTableView *tableView; 19 | 20 | @property (strong, nonatomic) NSArray *filteredParents; 21 | @property (strong, nonatomic) RACSubject *nameChanged; 22 | 23 | @end 24 | 25 | @implementation ASWAppDelegate 26 | 27 | @synthesize persistentStoreCoordinator = _persistentStoreCoordinator; 28 | 29 | @synthesize managedObjectModel = _managedObjectModel; 30 | @synthesize managedObjectContext = _managedObjectContext; 31 | 32 | - (void)applicationDidFinishLaunching:(NSNotification *)aNotification 33 | { 34 | // Need to do this for automatic detection of context 35 | [NSManagedObjectContext setMainContext:[self managedObjectContext]]; 36 | 37 | // Using RACCommand instead of target/action 38 | self.addButton.rac_command = [[RACCommand alloc] initWithSignalBlock:^(id _) { 39 | self.searchField.stringValue = @""; // reset search field on add 40 | 41 | // insert a new parent and set its default values 42 | // Of course, it might be better to do this in the model class itself 43 | // but it gives an example of using ReactiveCoreData 44 | return [RACSignal return: 45 | [Parent insert:^(Parent *parent) { 46 | parent.name = @"No name"; 47 | parent.age = 40; 48 | }]]; 49 | }]; 50 | 51 | // The signal holds the newly inserted parent, though we don't use it later. 52 | RACSignal *addedParent = self.addButton.rac_command.executionSignals; 53 | 54 | // Basically, implement a delegate method in a signal 55 | RACSignal *aParentIsSelected = [[self rac_signalForSelector:@selector(tableViewSelectionDidChange:)] 56 | map:^(id x) { 57 | return @(self.tableView.numberOfSelectedRows); 58 | }]; 59 | 60 | // Add it after the selector above is used, so that it gets called 61 | // Otherwise, need to subscribe to NSTableViewSelectionDidChangeNotification manually 62 | self.tableView.delegate = self; 63 | 64 | // Pretty straight-forward removal 65 | // I'd even say unnecessary long with the return of signal in addSignalBlock: 66 | // The good about having it a signal is that we can chain it later to react to deletion 67 | // See how this affects objectsChanged. 68 | 69 | self.removeButton.rac_command = [[RACCommand alloc] initWithEnabled:aParentIsSelected signalBlock:^RACSignal *(id _) { 70 | NSArray *objectsToRemove = [self.filteredParents objectsAtIndexes:self.tableView.selectedRowIndexes]; 71 | NSManagedObjectContext *context = [NSManagedObjectContext currentContext]; 72 | for (NSManagedObject *obj in objectsToRemove) { 73 | [context deleteObject:obj]; 74 | } 75 | return [RACSignal return:@YES]; 76 | }]; 77 | 78 | RACSignal *removedParent = self.removeButton.rac_command.executionSignals; 79 | 80 | // reload the data after filteredParents is updated 81 | [RACObserve(self, filteredParents) subscribeNext:^(id x) { 82 | [self.tableView reloadData]; 83 | }]; 84 | 85 | // we use this later to trigger refetch of the table 86 | // startWith is needed for the initial trigger on launch 87 | self.nameChanged = [RACSubject subject]; 88 | RACSignal *objectsChanged = [[RACSignal merge:@[addedParent, removedParent, self.nameChanged]] startWith:@YES]; 89 | 90 | // filterText will send next when the text in searchField changes either by user edit or direct update by us. 91 | RACSignal *filterText = [[RACSignal 92 | merge:@[self.searchField.rac_textSignal, RACObserve(self, searchField.stringValue)]] 93 | map:^id(id value) { 94 | return [value copy]; // just in case 95 | }]; 96 | 97 | // This part refetches data for the table and puts it into filteredParents 98 | // It either fetches all Parents or filters by name, if there's something in the search field 99 | // It will also refetch, if objectsChanged send a next 100 | RAC(self, filteredParents) = [[[[Parent findAll] 101 | where:@"name" contains:filterText options:@"cd"] 102 | sortBy:@"name"] 103 | fetchWithTrigger:objectsChanged]; 104 | 105 | // select the first row in the table 106 | [self.tableView selectRowIndexes:[NSIndexSet indexSetWithIndex:0] byExtendingSelection:NO]; 107 | } 108 | 109 | 110 | #pragma mark - NSTableView related 111 | - (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView; 112 | { 113 | return [self.filteredParents count]; 114 | } 115 | 116 | - (id)tableView:(NSTableView *)tableView objectValueForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row; 117 | { 118 | if (row < 0) return nil; 119 | NSUInteger r = (NSUInteger) row; 120 | 121 | if ([[tableColumn identifier] isEqualToString:@"Name"]) { 122 | return [self.filteredParents[r] name]; 123 | } 124 | return @([self.filteredParents[r] age]); 125 | } 126 | 127 | - (void)tableView:(NSTableView *)tableView setObjectValue:(id)object forTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row; 128 | { 129 | if (row < 0) return; 130 | Parent *parent = self.filteredParents[(NSUInteger) row]; 131 | if ([tableColumn.identifier isEqualToString:@"Name"]) { 132 | parent.name = object; 133 | [self.nameChanged sendNext:object]; 134 | } 135 | else { 136 | parent.age = [object integerValue]; 137 | } 138 | } 139 | 140 | #pragma mark - Boilerplate 141 | 142 | // Returns the directory the application uses to store the Core Data store file. This code uses a directory named "com.apparentsoft.ReactiveCoreData" in the user's Application Support directory. 143 | - (NSURL *)applicationFilesDirectory 144 | { 145 | NSFileManager *fileManager = [NSFileManager defaultManager]; 146 | NSURL *appSupportURL = [[fileManager URLsForDirectory:NSApplicationSupportDirectory inDomains:NSUserDomainMask] lastObject]; 147 | return [appSupportURL URLByAppendingPathComponent:@"com.apparentsoft.ReactiveCoreData"]; 148 | } 149 | 150 | // Creates if necessary and returns the managed object model for the application. 151 | - (NSManagedObjectModel *)managedObjectModel 152 | { 153 | if (_managedObjectModel) { 154 | return _managedObjectModel; 155 | } 156 | 157 | NSURL *modelURL = [[NSBundle mainBundle] URLForResource:@"ReactiveCoreData" withExtension:@"momd"]; 158 | _managedObjectModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL]; 159 | return _managedObjectModel; 160 | } 161 | 162 | // Returns the persistent store coordinator for the application. This implementation creates and return a coordinator, having added the store for the application to it. (The directory for the store is created, if necessary.) 163 | - (NSPersistentStoreCoordinator *)persistentStoreCoordinator 164 | { 165 | if (_persistentStoreCoordinator) { 166 | return _persistentStoreCoordinator; 167 | } 168 | 169 | NSManagedObjectModel *mom = [self managedObjectModel]; 170 | if (!mom) { 171 | NSLog(@"%@:%@ No model to generate a store from", [self class], NSStringFromSelector(_cmd)); 172 | return nil; 173 | } 174 | 175 | NSFileManager *fileManager = [NSFileManager defaultManager]; 176 | NSURL *applicationFilesDirectory = [self applicationFilesDirectory]; 177 | NSError *error = nil; 178 | 179 | NSDictionary *properties = [applicationFilesDirectory resourceValuesForKeys:@[NSURLIsDirectoryKey] error:&error]; 180 | 181 | if (!properties) { 182 | BOOL ok = NO; 183 | if ([error code] == NSFileReadNoSuchFileError) { 184 | ok = [fileManager createDirectoryAtPath:[applicationFilesDirectory path] withIntermediateDirectories:YES attributes:nil error:&error]; 185 | } 186 | if (!ok) { 187 | [[NSApplication sharedApplication] presentError:error]; 188 | return nil; 189 | } 190 | } else { 191 | if (![properties[NSURLIsDirectoryKey] boolValue]) { 192 | // Customize and localize this error. 193 | NSString *failureDescription = [NSString stringWithFormat:@"Expected a folder to store application data, found a file (%@).", [applicationFilesDirectory path]]; 194 | 195 | NSMutableDictionary *dict = [NSMutableDictionary dictionary]; 196 | [dict setValue:failureDescription forKey:NSLocalizedDescriptionKey]; 197 | error = [NSError errorWithDomain:@"YOUR_ERROR_DOMAIN" code:101 userInfo:dict]; 198 | 199 | [[NSApplication sharedApplication] presentError:error]; 200 | return nil; 201 | } 202 | } 203 | 204 | NSURL *url = [applicationFilesDirectory URLByAppendingPathComponent:@"ReactiveCoreData.storedata"]; 205 | NSPersistentStoreCoordinator *coordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:mom]; 206 | if (![coordinator addPersistentStoreWithType:NSXMLStoreType configuration:nil URL:url options:nil error:&error]) { 207 | [[NSApplication sharedApplication] presentError:error]; 208 | return nil; 209 | } 210 | _persistentStoreCoordinator = coordinator; 211 | 212 | return _persistentStoreCoordinator; 213 | } 214 | 215 | // Returns the managed object context for the application (which is already bound to the persistent store coordinator for the application.) 216 | - (NSManagedObjectContext *)managedObjectContext 217 | { 218 | if (_managedObjectContext) { 219 | return _managedObjectContext; 220 | } 221 | 222 | NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator]; 223 | if (!coordinator) { 224 | NSMutableDictionary *dict = [NSMutableDictionary dictionary]; 225 | [dict setValue:@"Failed to initialize the store" forKey:NSLocalizedDescriptionKey]; 226 | [dict setValue:@"There was an error building up the data file." forKey:NSLocalizedFailureReasonErrorKey]; 227 | NSError *error = [NSError errorWithDomain:@"YOUR_ERROR_DOMAIN" code:9999 userInfo:dict]; 228 | [[NSApplication sharedApplication] presentError:error]; 229 | return nil; 230 | } 231 | _managedObjectContext = [[NSManagedObjectContext alloc] init]; 232 | [_managedObjectContext setPersistentStoreCoordinator:coordinator]; 233 | 234 | return _managedObjectContext; 235 | } 236 | 237 | // Returns the NSUndoManager for the application. In this case, the manager returned is that of the managed object context for the application. 238 | - (NSUndoManager *)windowWillReturnUndoManager:(NSWindow *)window 239 | { 240 | return [[self managedObjectContext] undoManager]; 241 | } 242 | 243 | // Performs the save action for the application, which is to send the save: message to the application's managed object context. Any encountered errors are presented to the user. 244 | - (IBAction)saveAction:(id)sender 245 | { 246 | NSError *error = nil; 247 | 248 | if (![[self managedObjectContext] commitEditing]) { 249 | NSLog(@"%@:%@ unable to commit editing before saving", [self class], NSStringFromSelector(_cmd)); 250 | } 251 | 252 | if (![[self managedObjectContext] save:&error]) { 253 | [[NSApplication sharedApplication] presentError:error]; 254 | } 255 | } 256 | 257 | - (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender 258 | { 259 | // Save changes in the application's managed object context before the application terminates. 260 | 261 | if (!_managedObjectContext) { 262 | return NSTerminateNow; 263 | } 264 | 265 | if (![[self managedObjectContext] commitEditing]) { 266 | NSLog(@"%@:%@ unable to commit editing to terminate", [self class], NSStringFromSelector(_cmd)); 267 | return NSTerminateCancel; 268 | } 269 | 270 | if (![[self managedObjectContext] hasChanges]) { 271 | return NSTerminateNow; 272 | } 273 | 274 | NSError *error = nil; 275 | if (![[self managedObjectContext] save:&error]) { 276 | 277 | // Customize this code block to include application-specific recovery steps. 278 | BOOL result = [sender presentError:error]; 279 | if (result) { 280 | return NSTerminateCancel; 281 | } 282 | 283 | NSString *question = NSLocalizedString(@"Could not save changes while quitting. Quit anyway?", @"Quit without saves error question message"); 284 | NSString *info = NSLocalizedString(@"Quitting now will lose any changes you have made since the last successful save", @"Quit without saves error question info"); 285 | NSString *quitButton = NSLocalizedString(@"Quit anyway", @"Quit anyway button title"); 286 | NSString *cancelButton = NSLocalizedString(@"Cancel", @"Cancel button title"); 287 | NSAlert *alert = [[NSAlert alloc] init]; 288 | [alert setMessageText:question]; 289 | [alert setInformativeText:info]; 290 | [alert addButtonWithTitle:quitButton]; 291 | [alert addButtonWithTitle:cancelButton]; 292 | 293 | NSInteger answer = [alert runModal]; 294 | 295 | if (answer == NSAlertAlternateReturn) { 296 | return NSTerminateCancel; 297 | } 298 | } 299 | 300 | return NSTerminateNow; 301 | } 302 | 303 | @end 304 | -------------------------------------------------------------------------------- /ReactiveCoreDataApp/Parent.h: -------------------------------------------------------------------------------- 1 | // 2 | // Parent.h 3 | // ReactiveCoreData 4 | // 5 | // Created by Jacob Gorban on 26/04/2013. 6 | // Copyright (c) 2013 Apparent Software. All rights reserved. 7 | // 8 | 9 | #import 10 | #import 11 | 12 | @class Parent; 13 | 14 | @interface Parent : NSManagedObject 15 | 16 | @property (nonatomic) int32_t age; 17 | @property (nonatomic, retain) NSString * name; 18 | @property (nonatomic, retain) NSSet *children; 19 | @property (nonatomic, retain) Parent *spouse; 20 | 21 | + (NSString*)entityName; 22 | @end 23 | 24 | @interface Parent (CoreDataGeneratedAccessors) 25 | 26 | - (void)addChildrenObject:(NSManagedObject *)value; 27 | - (void)removeChildrenObject:(NSManagedObject *)value; 28 | - (void)addChildren:(NSSet *)values; 29 | - (void)removeChildren:(NSSet *)values; 30 | 31 | @end 32 | -------------------------------------------------------------------------------- /ReactiveCoreDataApp/Parent.m: -------------------------------------------------------------------------------- 1 | // 2 | // Parent.m 3 | // ReactiveCoreData 4 | // 5 | // Created by Jacob Gorban on 26/04/2013. 6 | // Copyright (c) 2013 Apparent Software. All rights reserved. 7 | // 8 | 9 | #import "Parent.h" 10 | 11 | @implementation Parent 12 | 13 | @dynamic age; 14 | @dynamic name; 15 | @dynamic children; 16 | @dynamic spouse; 17 | 18 | + (NSString *)entityName; 19 | { 20 | return @"Parent"; 21 | } 22 | 23 | 24 | @end 25 | -------------------------------------------------------------------------------- /ReactiveCoreDataApp/ReactiveCoreData-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | com.apparentsoft.${PRODUCT_NAME:rfc1034identifier} 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | ${PRODUCT_NAME} 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | 1 25 | LSMinimumSystemVersion 26 | ${MACOSX_DEPLOYMENT_TARGET} 27 | NSHumanReadableCopyright 28 | Copyright © 2013 Apparent Software. All rights reserved. 29 | NSMainNibFile 30 | MainMenu 31 | NSPrincipalClass 32 | NSApplication 33 | 34 | 35 | -------------------------------------------------------------------------------- /ReactiveCoreDataApp/ReactiveCoreData-Prefix.pch: -------------------------------------------------------------------------------- 1 | // 2 | // Prefix header for all source files of the 'ReactiveCoreData' target in the 'ReactiveCoreData' project 3 | // 4 | 5 | #ifdef __OBJC__ 6 | #import 7 | #endif 8 | -------------------------------------------------------------------------------- /ReactiveCoreDataApp/ReactiveCoreData.xcdatamodeld/.xccurrentversion: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | _XCCurrentVersionName 6 | ReactiveCoreData.xcdatamodel 7 | 8 | 9 | -------------------------------------------------------------------------------- /ReactiveCoreDataApp/ReactiveCoreData.xcdatamodeld/ReactiveCoreData.xcdatamodel/contents: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /ReactiveCoreDataApp/en.lproj/Credits.rtf: -------------------------------------------------------------------------------- 1 | {\rtf0\ansi{\fonttbl\f0\fswiss Helvetica;} 2 | {\colortbl;\red255\green255\blue255;} 3 | \paperw9840\paperh8400 4 | \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\ql\qnatural 5 | 6 | \f0\b\fs24 \cf0 Engineering: 7 | \b0 \ 8 | Some people\ 9 | \ 10 | 11 | \b Human Interface Design: 12 | \b0 \ 13 | Some other people\ 14 | \ 15 | 16 | \b Testing: 17 | \b0 \ 18 | Hopefully not nobody\ 19 | \ 20 | 21 | \b Documentation: 22 | \b0 \ 23 | Whoever\ 24 | \ 25 | 26 | \b With special thanks to: 27 | \b0 \ 28 | Mom\ 29 | } 30 | -------------------------------------------------------------------------------- /ReactiveCoreDataApp/en.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* Localized versions of Info.plist keys */ 2 | 3 | -------------------------------------------------------------------------------- /ReactiveCoreDataApp/main.m: -------------------------------------------------------------------------------- 1 | // 2 | // main.m 3 | // ReactiveCoreData 4 | // 5 | // Created by Jacob Gorban on 25/04/2013. 6 | // Copyright (c) 2013 Apparent Software. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | int main(int argc, char *argv[]) 12 | { 13 | return NSApplicationMain(argc, (const char **)argv); 14 | } 15 | -------------------------------------------------------------------------------- /ReactiveCoreDataTests/RACManagedObjectFetchSpecs.m: -------------------------------------------------------------------------------- 1 | // 2 | // RACManagedObjectFetchSpecs.m 3 | // ReactiveCoreData 4 | // 5 | // Created by Jacob Gorban on 25/04/2013. 6 | // Copyright (c) 2013 Apparent Software. All rights reserved. 7 | // 8 | 9 | #import 10 | #define EXP_SHORTHAND 11 | #import 12 | #import 13 | #import "NSManagedObject+ReactiveCoreData.h" 14 | #import "NSManagedObjectContext+ReactiveCoreData.h" 15 | #import "Parent.h" 16 | #import "RACSignal+ReactiveCoreData.h" 17 | 18 | NSManagedObjectContext * contextForTest(BOOL setAsMain) 19 | { 20 | NSPersistentStoreCoordinator *psc = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[NSManagedObjectModel mergedModelFromBundles:nil]]; 21 | NSError *error = nil; 22 | NSPersistentStore *persistentStore = [psc addPersistentStoreWithType:NSInMemoryStoreType configuration:nil URL:nil options:nil error:&error]; 23 | if (!persistentStore) { 24 | [[NSApplication sharedApplication] presentError:error]; 25 | return nil; 26 | } 27 | NSManagedObjectContext *ctx = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSConfinementConcurrencyType]; 28 | [ctx setPersistentStoreCoordinator:psc]; 29 | [ctx setUndoManager:nil]; 30 | if (setAsMain) { 31 | [NSManagedObjectContext setMainContext:ctx]; 32 | } 33 | [ctx save:NULL]; 34 | return ctx; 35 | } 36 | 37 | SpecBegin(RACMagagedObjectFetch) 38 | 39 | __block NSManagedObjectContext *ctx = nil; 40 | __block BOOL completed = NO; 41 | 42 | beforeEach(^{ 43 | ctx = contextForTest(YES); 44 | completed = NO; 45 | }); 46 | 47 | afterEach(^{ 48 | ctx = nil; 49 | [NSManagedObjectContext setMainContext:nil]; 50 | }); 51 | 52 | describe(@"NSManagedObject", ^{ 53 | __block BOOL executed; 54 | 55 | beforeEach(^{ 56 | executed = NO; 57 | }); 58 | 59 | it(@"creates a fetch request signal", ^{ 60 | RACSignal *signal = Parent.findAll; 61 | [signal subscribeNext:^(NSFetchRequest *req) { 62 | expect(req).toNot.beNil(); 63 | expect(req.entityName).to.equal(@"Parent"); 64 | executed = YES; 65 | }]; 66 | expect(executed).to.beTruthy(); 67 | }); 68 | 69 | it(@"inserts into context", ^{ 70 | Parent *parent = [Parent insert]; 71 | expect(parent).toNot.beNil(); 72 | expect(parent.managedObjectContext).to.equal(ctx); 73 | }); 74 | 75 | it(@"inserts with config block", ^{ 76 | Parent *parent = [Parent insert:^(Parent *obj) { 77 | obj.name = @"Daddy"; 78 | obj.age = 35; 79 | }]; 80 | expect(parent.name).to.equal(@"Daddy"); 81 | expect(parent.age).to.equal(35); 82 | }); 83 | 84 | it(@"findOne's fetch passes nil for empty result", ^{ 85 | [[[Parent findOne] fetch] subscribeNext:^(Parent *parent) { 86 | expect(parent).to.beNil(); 87 | completed = YES; 88 | }]; 89 | expect(completed).equal(YES); 90 | }); 91 | 92 | it(@"findOne's fetch passes object for non-empty result", ^{ 93 | [Parent insert:^(Parent *parent) { 94 | parent.name = @"One"; 95 | }]; 96 | 97 | [[[Parent findOne] fetch] subscribeNext:^(Parent *parent) { 98 | expect([parent name]).to.equal(@"One"); 99 | completed = YES; 100 | }]; 101 | expect(completed).equal(YES); 102 | }); 103 | }); 104 | 105 | describe(@"RACSignal", ^{ 106 | it(@"counts results", ^{ 107 | [Parent insert]; 108 | expect([[[Parent findAll] count] first]).to.equal(@1); 109 | }); 110 | 111 | it(@"fetches results", ^{ 112 | Parent *p1 = [Parent insert]; 113 | Parent *p2 = [Parent insert]; 114 | NSArray *result = [[[Parent findAll] fetch] first]; 115 | expect(result).to.contain(p1); 116 | expect(result).to.contain(p2); 117 | }); 118 | 119 | it(@"fetches with trigger", ^{ 120 | [Parent insert]; 121 | RACSubject *trigger = [RACSubject subject]; 122 | __block NSArray *actual; 123 | [[[[Parent findOne] fetchWithTrigger:trigger] collect] 124 | subscribeNext:^(id x) { 125 | actual = x; 126 | completed = YES; 127 | }]; 128 | [trigger sendNext:@1]; 129 | [trigger sendNext:@1]; 130 | [trigger sendCompleted]; 131 | expect(completed).to.beTruthy(); 132 | expect(actual).to.haveCountOf(2); 133 | }); 134 | 135 | it(@"starts a findAll fetch request for entity name", ^{ 136 | __block NSFetchRequest *actual; 137 | [[[RACSignal return:@1] findAll:Parent.entityName] 138 | subscribeNext:^(id x){ 139 | completed = YES; 140 | actual = x; 141 | }]; 142 | expect(completed).to.beTruthy(); 143 | expect(actual).to.beKindOf([NSFetchRequest class]); 144 | expect([actual entityName]).to.equal([Parent entityName]); 145 | }); 146 | 147 | it(@"starts a findOne fetch request for entity name", ^{ 148 | __block NSFetchRequest *actual; 149 | [[[RACSignal return:@1] findOne:Parent.entityName] 150 | subscribeNext:^(id x){ 151 | completed = YES; 152 | actual = x; 153 | }]; 154 | expect(completed).to.beTruthy(); 155 | expect(actual).to.beKindOf([NSFetchRequest class]); 156 | expect([actual entityName]).to.equal([Parent entityName]); 157 | expect([actual fetchLimit]).to.equal(1); 158 | }); 159 | 160 | }); 161 | 162 | describe(@"FetchRequest operations:", ^{ 163 | __block Parent *Joe; 164 | __block Parent *Jane; 165 | beforeEach(^{ 166 | Joe = [Parent insert]; 167 | Jane = [Parent insert]; 168 | Joe.name = @"Joe"; 169 | Jane.name = @"Jane"; 170 | Joe.age = 40; 171 | Jane.age = 35; 172 | }); 173 | 174 | it(@"updates fetch request with a constant predicate", ^{ 175 | NSPredicate *predicate = [NSPredicate predicateWithFormat:@"name == 'Jane'"]; 176 | NSArray *result = [[[[Parent findAll] where:predicate] fetch] first]; 177 | expect(result).to.equal(@[ Jane ]); 178 | }); 179 | 180 | it(@"updates fetch request with a predicate signal", ^{ 181 | RACSubject *predicateSignal = [RACSubject subject]; 182 | __block id final_result; 183 | 184 | [[[[[Parent findAll] where:predicateSignal] fetch] collect] subscribeNext:^(id x) { 185 | final_result = x; 186 | }]; 187 | 188 | [predicateSignal sendNext:[NSPredicate predicateWithFormat:@"name == 'Jane'"]]; 189 | [predicateSignal sendNext:[NSPredicate predicateWithFormat:@"name == 'Joe'"]]; 190 | [predicateSignal sendCompleted]; 191 | 192 | expect(final_result).to.equal((@[ @[ Jane ], @[ Joe ] ])); 193 | }); 194 | 195 | it(@"where for property constant value", ^{ 196 | NSArray *result = [[[Parent.findAll where:@"name" equals:@"Jane"] fetch] first]; 197 | expect(result).to.equal(@[Jane]); 198 | }); 199 | 200 | it(@"where for property signal", ^{ 201 | RACSubject *nameSignal = [RACSubject subject]; 202 | __block id final_result; 203 | [[[[Parent.findAll where:@"name == %@" args:@[nameSignal]] fetch] collect] 204 | subscribeNext:^(id x) { 205 | final_result = x; 206 | }]; 207 | 208 | [nameSignal sendNext:@"Jane"]; 209 | [nameSignal sendNext:@"Joe"]; 210 | [nameSignal sendCompleted]; 211 | 212 | NSArray *exp = @[@[Jane], @[Joe]]; 213 | expect(final_result).to.equal(exp); 214 | }); 215 | 216 | it(@"creates a correct fetch in where:contains:options: with nil options", ^{ 217 | __block NSFetchRequest *actual; 218 | [[[Parent findAll] where:@"name" contains:@"value" options:nil] subscribeNext:^(id x) { 219 | actual = x; 220 | completed = YES; 221 | }]; 222 | expect(completed).to.beTruthy(); 223 | expect(actual.predicate.predicateFormat).to.equal(@"name CONTAINS \"value\""); 224 | }); 225 | 226 | it(@"returns fetch without a predicate in where:contains:options: with an empty value", ^{ 227 | __block NSFetchRequest *actual; 228 | [[[Parent findAll] where:@"name" contains:@"" options:nil] subscribeNext:^(id x) { 229 | actual = x; 230 | completed = YES; 231 | }]; 232 | expect(completed).to.beTruthy(); 233 | expect(actual.predicate).to.beNil(); 234 | }); 235 | 236 | it(@"returns a correct fetch predicate in where:contains:options: with an non-nil options", ^{ 237 | __block NSFetchRequest *actual; 238 | [[[Parent findAll] where:@"name" contains:@"value" options:@"cd"] subscribeNext:^(id x) { 239 | actual = x; 240 | completed = YES; 241 | }]; 242 | expect(completed).to.beTruthy(); 243 | expect(actual.predicate.predicateFormat).to.equal(@"name CONTAINS[cd] \"value\""); 244 | }); 245 | 246 | 247 | it(@"sends complete", ^{ 248 | [[Parent.findAll fetch] 249 | subscribeNext:^(id x) { 250 | } 251 | completed:^{ 252 | completed = YES; 253 | }]; 254 | expect(completed).to.beTruthy(); 255 | }); 256 | 257 | it(@"handles predicates for constants", ^{ 258 | NSArray *result = [[[Parent.findAll where:@"name == %@" args:@[@"Jane"]] fetch] first]; 259 | expect(result).to.equal(@[Jane]); 260 | }); 261 | 262 | context(@"check limits", ^{ 263 | beforeEach(^{ 264 | for (NSUInteger i=0; i<50; i++) { 265 | [Parent insert]; 266 | } 267 | }); 268 | 269 | it(@"number limits", ^{ 270 | [[[Parent.findAll limit:@10] fetch] 271 | subscribeNext:^(id x) { 272 | expect(x).to.haveCountOf(10); 273 | completed = YES; 274 | }]; 275 | expect(completed).to.beTruthy(); 276 | }); 277 | 278 | it(@"limit signals", ^{ 279 | RACSubject *limitSignal = [RACSubject subject]; 280 | __block NSArray *final_result; 281 | [[[[Parent.findAll limit:limitSignal] fetch] collect] 282 | subscribeNext:^(id x) { 283 | final_result = x; 284 | }]; 285 | [limitSignal sendNext:@10]; 286 | [limitSignal sendNext:@30]; 287 | [limitSignal sendCompleted]; 288 | expect(final_result).to.haveCountOf(2); 289 | expect(final_result[0]).to.haveCountOf(10); 290 | expect(final_result[1]).to.haveCountOf(30); 291 | }); 292 | }); 293 | 294 | it(@"can return NSManagedObjectIDResultType", ^{ 295 | [[[[Parent findAll] IDResultType] fetch] subscribeNext:^(NSArray *result) { 296 | expect(result).to.haveCountOf(2); 297 | expect([result lastObject]).to.beKindOf([NSManagedObjectID class]); 298 | completed = YES; 299 | }]; 300 | expect(completed).to.beTruthy(); 301 | }); 302 | 303 | it(@"converts an ObjectID-type fetched array result to objects", ^{ 304 | [[[[[Parent findAll] IDResultType] fetch] objectIDsToObjects] subscribeNext:^(NSArray *result) { 305 | expect(result).to.contain(Joe); 306 | expect(result).to.contain(Jane); 307 | completed = YES; 308 | }]; 309 | expect(completed).to.beTruthy(); 310 | }); 311 | 312 | it(@"updates the sort of a fetch request for one constant sort descriptor", ^{ 313 | NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"name" ascending:YES]; 314 | [[[Parent findAll] sortBy:sortDescriptor] subscribeNext:^(NSFetchRequest *actual) { 315 | expect(actual.sortDescriptors).to.equal(@[sortDescriptor]); 316 | completed = YES; 317 | }]; 318 | expect(completed).to.beTruthy(); 319 | }); 320 | 321 | it(@"updates the sort of a fetch request for an array of sort descriptors", ^{ 322 | NSSortDescriptor *sortByName = [NSSortDescriptor sortDescriptorWithKey:@"name" ascending:YES]; 323 | NSSortDescriptor *sortByAge = [NSSortDescriptor sortDescriptorWithKey:@"age" ascending:YES]; 324 | NSArray *sortDescriptors = @[ sortByName, sortByAge ]; 325 | [[[Parent findAll] sortBy:sortDescriptors] subscribeNext:^(NSFetchRequest *actual) { 326 | expect(actual.sortDescriptors).to.equal(sortDescriptors); 327 | completed = YES; 328 | }]; 329 | expect(completed).to.beTruthy(); 330 | }); 331 | 332 | it(@"updates the sort of a fetch request for a signal with an array of sort descriptors", ^{ 333 | NSSortDescriptor *sortByName = [NSSortDescriptor sortDescriptorWithKey:@"name" ascending:YES]; 334 | NSSortDescriptor *sortByAge = [NSSortDescriptor sortDescriptorWithKey:@"age" ascending:YES]; 335 | NSArray *sortDescriptors1 = @[ sortByName, sortByAge ]; 336 | NSArray *sortDescriptors2 = @[ sortByAge, sortByName ]; 337 | RACSubject *sortDescriptors = [RACSubject subject]; 338 | [[[[[Parent findAll] sortBy:sortDescriptors] 339 | map:^id(NSFetchRequest *request) { 340 | return request.sortDescriptors; 341 | }] 342 | collect] 343 | subscribeNext:^(NSArray *actual) { 344 | expect(actual[0]).to.equal(sortDescriptors1); 345 | expect(actual[1]).to.equal(sortDescriptors2); 346 | expect([actual[2][0] ascending]).to.beFalsy(); 347 | expect([actual[2][0] key]).to.equal(@"name"); 348 | completed = YES; 349 | }]; 350 | [sortDescriptors sendNext:sortDescriptors1]; 351 | [sortDescriptors sendNext:sortDescriptors2]; 352 | [sortDescriptors sendNext:@"-name"]; 353 | [sortDescriptors sendCompleted]; 354 | expect(completed).to.beTruthy(); 355 | }); 356 | 357 | it(@"updates the sort of a fetch request ascending for a key string value", ^{ 358 | NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"name" ascending:YES]; 359 | [[[Parent findAll] sortBy:@"name"] subscribeNext:^(NSFetchRequest *actual) { 360 | expect(actual.sortDescriptors).to.equal(@[sortDescriptor]); 361 | completed = YES; 362 | }]; 363 | expect(completed).to.beTruthy(); 364 | }); 365 | 366 | it(@"updates the sort of a fetch request descending for a -key string value", ^{ 367 | NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"name" ascending:NO]; 368 | [[[Parent findAll] sortBy:@"-name"] subscribeNext:^(NSFetchRequest *actual) { 369 | expect(actual.sortDescriptors).to.equal(@[sortDescriptor]); 370 | completed = YES; 371 | }]; 372 | expect(completed).to.beTruthy(); 373 | }); 374 | 375 | }); 376 | 377 | describe(@"Cross-Thread functionality", ^{ 378 | it(@"Creates a new background context", ^{ 379 | [[[RACSignal empty] 380 | performInBackgroundContext] 381 | subscribeCompleted:^{ 382 | NSManagedObjectContext *moc = [NSManagedObjectContext currentContext]; 383 | expect(moc).toNot.equal(ctx); 384 | completed = YES; 385 | }]; 386 | expect(completed).will.beTruthy(); 387 | }); 388 | 389 | it(@"Merges changes from background context", ^AsyncBlock{ 390 | [[[[[[RACSignal return:@"empty"] 391 | performInBackgroundContext] 392 | doNext:^(id _) { 393 | Parent *dad = [Parent insert]; 394 | dad.name = @"Dad"; 395 | }] 396 | saveContext] 397 | deliverOn:RACScheduler.mainThreadScheduler] 398 | subscribeNext:^(id _){ 399 | [[[Parent findAll] fetch] 400 | subscribeNext:^(NSArray *result) { 401 | expect([[result lastObject] name]).to.equal(@"Dad"); 402 | completed = YES; 403 | done(); 404 | }]; 405 | }]; 406 | expect(completed).will.beTruthy(); 407 | }); 408 | 409 | it(@"Has a signal that sends after a merge", ^AsyncBlock{ 410 | __block BOOL local_completed = NO; 411 | id d1 = [[[[RACSignal return:@"empty"] 412 | performInBackgroundContext:^(NSManagedObjectContext *context) { 413 | [Parent insert]; 414 | }] 415 | saveContext] 416 | subscribeNext:^(id x) { 417 | }]; 418 | id d2 = [ctx.rcd_merged subscribeNext:^(NSNotification *note){ 419 | local_completed = YES; 420 | expect([note userInfo][NSInsertedObjectsKey]).to.haveCountOf(1); 421 | done(); 422 | }]; 423 | expect(local_completed).will.beTruthy(); 424 | expect(d1).toNot.beNil(); 425 | expect(d2).toNot.beNil(); 426 | }); 427 | 428 | it(@"Has a signal that sends after a save", ^{ 429 | NSManagedObjectContext *context = ctx; 430 | [ctx.rcd_saved subscribeNext:^(id x) { 431 | expect([x object]).to.equal(context); 432 | completed = YES; 433 | }]; 434 | 435 | [Parent insert]; 436 | [ctx save:NULL]; 437 | expect(completed).will.beTruthy(); 438 | }); 439 | }); 440 | 441 | describe(@"Document-based contexts", ^{ 442 | __block NSManagedObjectContext *doc1ctx = nil; 443 | __block NSManagedObjectContext *doc2ctx = nil; 444 | 445 | beforeEach(^{ 446 | doc1ctx = contextForTest(NO); 447 | doc2ctx = contextForTest(NO); 448 | }); 449 | 450 | it(@"can perform on specific context", ^{ 451 | expect([NSManagedObjectContext currentContext]).to.equal(ctx); 452 | [[doc1ctx perform] 453 | subscribeNext:^(NSManagedObjectContext *context1) { 454 | expect(context1).to.equal(doc1ctx); 455 | expect([NSManagedObjectContext currentContext]).to.equal(doc1ctx); 456 | completed = YES; 457 | }]; 458 | expect([NSManagedObjectContext currentContext]).to.equal(ctx); 459 | expect(completed).to.beTruthy(); 460 | }); 461 | 462 | it(@"can perform on two specific contexts", ^{ 463 | expect([NSManagedObjectContext currentContext]).to.equal(ctx); 464 | [[[doc1ctx perform] 465 | doNext:^(id _) { 466 | Parent *dad = [Parent insert]; 467 | dad.name = @"dad"; 468 | }] 469 | subscribeNext:^(NSManagedObjectContext *context1) { 470 | }]; 471 | 472 | [[[doc2ctx perform] 473 | doNext:^(id _) { 474 | Parent *mom = [Parent insert]; 475 | mom.name = @"mom"; 476 | }] 477 | subscribeNext:^(NSManagedObjectContext *context1) { 478 | }]; 479 | expect([NSManagedObjectContext currentContext]).to.equal(ctx); 480 | NSFetchRequest *req1 = [NSFetchRequest fetchRequestWithEntityName:[Parent entityName]]; 481 | NSFetchRequest *req2 = [NSFetchRequest fetchRequestWithEntityName:[Parent entityName]]; 482 | NSArray *parents1 = [doc1ctx executeFetchRequest:req1 error:NULL]; 483 | NSArray *parents2 = [doc2ctx executeFetchRequest:req2 error:NULL]; 484 | expect([parents1.lastObject name]).to.equal(@"dad"); 485 | expect([parents2.lastObject name]).to.equal(@"mom"); 486 | }); 487 | 488 | it(@"deallocate the perform chain", ^{ 489 | __block BOOL deallocated = NO; 490 | @autoreleasepool { 491 | RACDisposable *disposable = [[doc1ctx perform] 492 | subscribeNext:^(NSManagedObjectContext *context1) { 493 | expect(deallocated).to.beFalsy(); 494 | }]; 495 | [disposable.rac_deallocDisposable addDisposable:[RACDisposable disposableWithBlock:^{ 496 | deallocated = YES; 497 | }]]; 498 | } 499 | expect(deallocated).to.beTruthy(); 500 | }); 501 | 502 | it(@"creates a child of a document context", ^{ 503 | __block volatile uint32_t done = 0; 504 | NSMutableDictionary *expected = [[NSMutableDictionary alloc] initWithCapacity:5]; 505 | [[[[[doc1ctx perform] 506 | doNext:^(id x) { 507 | Parent *dad = [Parent insert]; 508 | dad.name = @"dad"; 509 | }] 510 | saveContext] 511 | performInBackgroundContext] 512 | subscribeNext:^(id x){ 513 | NSManagedObjectContext *currentContext = [NSManagedObjectContext currentContext]; 514 | [expected setValue:currentContext forKey:@"context"]; 515 | [expected setValue:[currentContext mainContext] forKey:@"mainContext"]; 516 | [[[Parent findAll] fetch] 517 | subscribeNext:^(NSArray *result) { 518 | [expected setValue:result forKey:@"result"]; 519 | OSAtomicOr32Barrier(1, &done); 520 | }]; 521 | }]; 522 | while (OSAtomicAnd32Barrier(1, &done) == 0) { usleep(10000); }; 523 | expect(expected[@"context"]).toNot.beNil(); 524 | expect(expected[@"mainContext"]).to.equal(doc1ctx); 525 | NSArray *result = expected[@"result"]; 526 | expect(result).to.haveCountOf(1); 527 | expect([[result lastObject] name]).to.equal(@"dad"); 528 | }); 529 | 530 | it(@"has performInBackground for context instance", ^AsyncBlock { 531 | __block NSManagedObjectContext *actualMain; 532 | [[doc1ctx performInBackground] 533 | subscribeNext:^(id x) { 534 | actualMain = [[NSManagedObjectContext currentContext] mainContext]; 535 | done(); 536 | }]; 537 | expect(actualMain).will.equal(doc1ctx); 538 | }); 539 | 540 | it(@"has performInContext", ^{ 541 | [[[RACSignal return:@1] 542 | performInContext:doc1ctx] 543 | subscribeNext:^(id x) { 544 | Parent *dad = [Parent insert]; 545 | dad.name = @"dad"; 546 | }]; 547 | 548 | __block NSArray *actual; 549 | [[[Parent findAll] fetchInMOC:doc1ctx] 550 | subscribeNext:^(NSArray *result) { 551 | actual = result; 552 | }]; 553 | 554 | expect(actual).to.haveCountOf(1); 555 | expect([[actual lastObject] name]).to.equal(@"dad"); 556 | }); 557 | }); 558 | 559 | SpecEnd 560 | 561 | -------------------------------------------------------------------------------- /ReactiveCoreDataTests/ReactiveCoreDataTests-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | com.apparentsoft.${PRODUCT_NAME:rfc1034identifier} 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundlePackageType 14 | BNDL 15 | CFBundleShortVersionString 16 | 1.0 17 | CFBundleSignature 18 | ???? 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /ReactiveCoreDataTests/en.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* Localized versions of Info.plist keys */ 2 | 3 | --------------------------------------------------------------------------------