Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Newer
Older
100644 302 lines (170 sloc) 15.634 kb
b1314b7 @casademora File reorg, updating the README
casademora authored
1 # ![Awesome](https://github.com/magicalpanda/magicalpanda.github.com/blob/master/images/awesome_logo_small.png?raw=true) MagicalRecord
dc6ab2c @casademora updated readme, changed to markdown
casademora authored
2
3 In software engineering, the active record pattern is a design pattern found in software that stores its data in relational databases. It was named by Martin Fowler in his book Patterns of Enterprise Application Architecture. The interface to such an object would include functions such as Insert, Update, and Delete, plus properties that correspond more-or-less directly to the columns in the underlying database table.
4
b1314b7 @casademora File reorg, updating the README
casademora authored
5 > Active record is an approach to accessing data in a database. A database table or view is wrapped into a class; thus an object instance is tied to a single row in the table. After creation of an object, a new row is added to the table upon save. Any object loaded gets its information from the database; when an object is updated, the corresponding row in the table is also updated. The wrapper class implements accessor methods or properties for each column in the table or view.
dc6ab2c @casademora updated readme, changed to markdown
casademora authored
6
7 > *- [Wikipedia]("http://en.wikipedia.org/wiki/Active_record_pattern")*
8
468b2d1 @casademora Updating threading instructions
casademora authored
9 MagicalRecord was inspired by the ease of Ruby on Rails' Active Record fetching. The goals of this code are:
dc6ab2c @casademora updated readme, changed to markdown
casademora authored
10
11 * Clean up my Core Data related code
12 * Allow for clear, simple, one-line fetches
13 * Still allow the modification of the NSFetchRequest when request optimizations are needed
14
38515e7 @casademora Minor Readme Updates
casademora authored
15
dc6ab2c @casademora updated readme, changed to markdown
casademora authored
16 # Installation
17
8c3540b @casademora Minor Readme Updates
casademora authored
18 1. In your XCode Project, drag the *MagicalRecord* folder (under the main folder) into your project.
b9e3546 @casademora Updating documentation
casademora authored
19 2. Add *CoreData+MagicalRecord.h* file to your PCH file or your AppDelegate file.
f99215b Correct typo
Ryan Maxwell authored
20 3. Optionally preceed the *CoreData+MagicalRecord.h* import with `#define MR_SHORTHAND` to your PCH file if you want to use MagicalRecord methods without the *MR_prefix* like `findAll` instead of `MR_findAll`
b1314b7 @casademora File reorg, updating the README
casademora authored
21 4. Start writing code!
dc6ab2c @casademora updated readme, changed to markdown
casademora authored
22
acff79a @casademora Make platform requirements more explicit
casademora authored
23 # Requirements
24
25 MagicalRecord Platform Requirements:
26
27 * iOS5.0 and newer, or Mac OS 10.7 and newer
28 * ARC
29
30 An iOS4 compatible version is available for use. Reference [tag 1.8.3](https://github.com/magicalpanda/MagicalRecord/tree/1.8.3).
31
32 ## Updating to 2.0
33
34 MagicalRecord 2.0 is considered a major update since there were some class and API refactorings that will effect previous installations of MagicalRecord in your code. The most straight forward change is that *MagicalRecordHelpers* and *MRCoreDataAction* have both been replaced with a single class, *MagicalRecord*.
35
36 ## ARC Support
37
38 MagicalRecord fully supports ARC out of the box, there is no configuration necessary.
39 The last version to support manually managed memory is 1.8.3, and is available from the downloads page, or by switching to the 1.8.3 tag in the source.
40
41
b1314b7 @casademora File reorg, updating the README
casademora authored
42 # Notes
8c3540b @casademora Minor Readme Updates
casademora authored
43 ## Third Party Blog Entries
44 The following blog entries highlight how to install and use aspects of Magical Record.
45
46 * [How to make Programming with Core Data Pleasant](http://yannickloriot.com/2012/03/magicalrecord-how-to-make-programming-with-core-data-pleasant/)
47 * [Using Core Data with MagicalRecord](http://ablfx.com/blog/2012/03/using-coredata-magicalrecord/)
48 * [Super Happy Easy Fetching in Core Data](http://www.cimgf.com/2011/03/13/super-happy-easy-fetching-in-core-data/)
49 * [Core Data and Threads, without the Headache](http://www.cimgf.com/2011/05/04/core-data-and-threads-without-the-headache/)
50 * [Unit Testing with Core Data](http://www.cimgf.com/2012/05/15/unit-testing-with-core-data/)
51
52 ## Twitter
53 Follow [@MagicalRecord](http://twitter.com/magicalrecord) on twitter to stay up to date on twitter with the lastest updates to MagicalRecord and for basic support
54
b1314b7 @casademora File reorg, updating the README
casademora authored
55
56 ## Nested Contexts
57
8c3540b @casademora Minor Readme Updates
casademora authored
58 New in Core Data is support for related contexts. This is a super neat, and super fast feature. However, writing a wrapper that supports both is, frankly, more work that it's worth. However, the 1.8.3 version will be the last version that has dual support, and going forward, MagicalRecord will only work with the version of Core Data that has supports nested managed object contexts.
d66b419 @casademora Updated some docs
casademora authored
59
2bd21c4 @casademora Add logging information
casademora authored
60 MagicalRecord provides a background saving queue so that saving all data is performed off the main thread, in the background. This means that it may be necessary to use *MR_saveNestedContexts* rather than the typical *MR_save* method in order to persist your changes all the way to your persistent store;
61
62 ## Logging
63 MagicalRecord has logging built in to every fetch request and other Core Data operation. When errors occur when fetching or saving data, these errors are captured by MagicalRecord. By default, these logs use NSLog to present logging information. However, if you have CocoaLumberjack installed in your project, MagicalRecord will use CocoaLumberjack and it's configuration to send logs to their proper output.
64
65 All logging in MagicalRecord can be disabled by placing this define preprocessor statement prior to the main import of CoreData+MagicalRecord.h
66
67 #define MR_ENABLE_ACTIVE_RECORD_LOGGING 0
68
dc6ab2c @casademora updated readme, changed to markdown
casademora authored
69 # Usage
70
71 ## Setting up the Core Data Stack
72
1a4f8d6 @casademora Removing references to ActiveRecord, renaming to MagicalRecord
casademora authored
73 To get started, first, import the header file *CoreData+MagicalRecord.h* in your project's pch file. This will allow a global include of all the required headers.
4e3f9fb @yas375 fixed some markdown issues with `*` instead of `\*`
yas375 authored
74 Next, somewhere in your app delegate, in either the applicationDidFinishLaunching:(UIApplication \*) withOptions:(NSDictionary \*) method, or awakeFromNib, use **one** of the following setup calls with the **MagicalRecord** class:
dc6ab2c @casademora updated readme, changed to markdown
casademora authored
75
76 + (void) setupCoreDataStack;
77 + (void) setupAutoMigratingDefaultCoreDataStack;
78 + (void) setupCoreDataStackWithInMemoryStore;
79 + (void) setupCoreDataStackWithStoreNamed:(NSString *)storeName;
80 + (void) setupCoreDataStackWithAutoMigratingSqliteStoreNamed:(NSString *)storeName;
cce797d @casademora Update threading instructions
casademora authored
81
b9e3546 @casademora Updating documentation
casademora authored
82 Each call instantiates one of each piece of the Core Data stack, and provides getter and setter methods for these instances. These well known instances to MagicalRecord, and are recognized as "defaults".
dc6ab2c @casademora updated readme, changed to markdown
casademora authored
83
8842d8f @blackgold9 Clarified the DEBUG only nature of the fix
blackgold9 authored
84 When using the default sqlite data store with the DEBUG flag set, if you change your model without creating a new model version, Magical Record will delete the old store and create a new one automatically. No more uninstall/reinstall every time you make a change!
d8394cf @blackgold9 Updating readme with a short blurb
blackgold9 authored
85
86 And finally, before your app exits, you can use the clean up method:
dc6ab2c @casademora updated readme, changed to markdown
casademora authored
87
468b2d1 @casademora Updating threading instructions
casademora authored
88 [MagicalRecord cleanUp];
9dddc5d @emrosenf [README] Provide iCloud details
emrosenf authored
89
90 ## iCloud Support
91
468b2d1 @casademora Updating threading instructions
casademora authored
92 Apps built for iOS5+ and OSX Lion 10.7.2+ can take advantage of iCloud to sync Core Data stores. To implement this functionality with MagicalRecord, use **one** of the following setup calls instead of those listed in the previous section:
9dddc5d @emrosenf [README] Provide iCloud details
emrosenf authored
93
051c8fb Update README.md
Evan Rosenfeld authored
94 + (void) setupCoreDataStackWithiCloudContainer:(NSString *)icloudBucket localStoreNamed:(NSString *)localStore;
95 + (void) setupCoreDataStackWithiCloudContainer:(NSString *)containerID contentNameKey:(NSString *)contentNameKey localStoreNamed:(NSString *)localStoreName cloudStorePathComponent:(NSString *)pathSubcomponent;
96 + (void) setupCoreDataStackWithiCloudContainer:(NSString *)containerID contentNameKey:(NSString *)contentNameKey localStoreNamed:(NSString *)localStoreName cloudStorePathComponent:(NSString *)pathSubcomponent completion:(void(^)(void))completion;
9dddc5d @emrosenf [README] Provide iCloud details
emrosenf authored
97
adad524 Update README.md
Evan Rosenfeld authored
98 For further details, and to ensure that your application is suitable for iCloud, please see [Apple's iCloud Notes](https://developer.apple.com/library/ios/#releasenotes/DataManagement/RN-iCloudCoreData/_index.html).
99
4e3f9fb @yas375 fixed some markdown issues with `*` instead of `\*`
yas375 authored
100 In particular note that the first helper method, + (void) setupCoreDataStackWithiCloudContainer:(NSString \*)icloudBucket localStoreNamed:(NSString \*)localStore, automatically generates the **NSPersistentStoreUbiquitousContentNameKey** based on your application's Bundle Identifier.
adad524 Update README.md
Evan Rosenfeld authored
101
102 If you are managing multiple different iCloud stores it is highly recommended that you use one of the other helper methods to specify your own **contentNameKey**
dc6ab2c @casademora updated readme, changed to markdown
casademora authored
103
104 ### Default Managed Object Context
105
2bd21c4 @casademora Add logging information
casademora authored
106 When using Core Data, you will deal with two types of objects the most: *NSManagedObject* and *NSManagedObjectContext*. MagicalRecord provides a single place for a default NSManagedObjectContext for use within your app. This is great for single threaded apps. You can easily get to this default context by calling:
dc6ab2c @casademora updated readme, changed to markdown
casademora authored
107
5470cc6 @casademora Made the default managed object context section more clear.
casademora authored
108 [NSManagedObjectContext MR_defaultContext];
dc6ab2c @casademora updated readme, changed to markdown
casademora authored
109
5470cc6 @casademora Made the default managed object context section more clear.
casademora authored
110 This context will be used if a find or request method (described below) is not specifying a specific context using the **inContext:** method overload.
dc6ab2c @casademora updated readme, changed to markdown
casademora authored
111
5470cc6 @casademora Made the default managed object context section more clear.
casademora authored
112 If you need to create a new Managed Object Context for use in other threads, based on the default persistent store that was creating using one of the setup methods, use:
dc6ab2c @casademora updated readme, changed to markdown
casademora authored
113
b1314b7 @casademora File reorg, updating the README
casademora authored
114 NSManagedObjectContext *myNewContext = [NSManagedObjectContext MR_context];
5470cc6 @casademora Made the default managed object context section more clear.
casademora authored
115
116 This will use the same object model and persistent store, but create an entirely new context for use with threads other than the main thread.
117
118 And, if you want to make *myNewContext* the default for all fetch requests on the main thread:
b9e3546 @casademora Updating documentation
casademora authored
119
b1314b7 @casademora File reorg, updating the README
casademora authored
120 [NSManagedObjectContext MR_setDefaultContext:myNewContext];
dc6ab2c @casademora updated readme, changed to markdown
casademora authored
121
468b2d1 @casademora Updating threading instructions
casademora authored
122 MagicalRecord also has a helper method to hold on to a Managed Object Context in a thread's threadDictionary. This lets you access the correct NSManagedObjectContext instance no matter which thread you're calling from. This methods is:
dc6ab2c @casademora updated readme, changed to markdown
casademora authored
123
5470cc6 @casademora Made the default managed object context section more clear.
casademora authored
124 [NSManagedObjectContext MR_contextForCurrentThread];
dc6ab2c @casademora updated readme, changed to markdown
casademora authored
125
b9e3546 @casademora Updating documentation
casademora authored
126 **It is *highly* recommended that the default context is created and set using the main thread**
dc6ab2c @casademora updated readme, changed to markdown
casademora authored
127
128 ### Fetching
129
130 #### Basic Finding
b9e3546 @casademora Updating documentation
casademora authored
131
132 Most methods in MagicalRecord return an NSArray of results. So, if you have an Entity called Person, related to a Department (as seen in various Apple Core Data documentation), to get all the Person entities from your Persistent Store:
dc6ab2c @casademora updated readme, changed to markdown
casademora authored
133
feffba8 @tonyxiao Update Readme.md to reflect recent changes, especially the MR_SHORTHAND ...
tonyxiao authored
134 //In order for this to work you need to add "#define MR_SHORTHAND" to your PCH file
8c14cc7 @maikg Fix small error in README with regard to MR_SHORTHAND
maikg authored
135 NSArray *people = [Person findAll];
dc6ab2c @casademora updated readme, changed to markdown
casademora authored
136
feffba8 @tonyxiao Update Readme.md to reflect recent changes, especially the MR_SHORTHAND ...
tonyxiao authored
137 // Otherwise you can use the longer, namespaced version
138 NSArray *people = [Person MR_findAll];
dc6ab2c @casademora updated readme, changed to markdown
casademora authored
139
140 Or, to have the results sorted by a property:
141
b1314b7 @casademora File reorg, updating the README
casademora authored
142 NSArray *peopleSorted = [Person MR_findAllSortedByProperty:@"LastName" ascending:YES];
dc6ab2c @casademora updated readme, changed to markdown
casademora authored
143
26d74ec @casademora Adding aggregate operation support submitted from Duane Fields. Thanks f...
casademora authored
144 Or, to have the results sorted by multiple properties:
145
b1314b7 @casademora File reorg, updating the README
casademora authored
146 NSArray *peopleSorted = [Person MR_findAllSortedByProperty:@"LastName,FirstName" ascending:YES];
dc6ab2c @casademora updated readme, changed to markdown
casademora authored
147
148 If you have a unique way of retrieving a single object from your data store, you can get that object directly:
149
b1314b7 @casademora File reorg, updating the README
casademora authored
150 Person *person = [Person MR_findFirstByAttribute:@"FirstName" withValue:@"Forrest"];
dc6ab2c @casademora updated readme, changed to markdown
casademora authored
151
152 #### Advanced Finding
153
154 If you want to be more specific with your search, you can send in a predicate:
155
156 NSArray *departments = [NSArray arrayWithObjects:dept1, dept2, ..., nil];
157 NSPredicate *peopleFilter = [NSPredicate predicateWithFormat:@"Department IN %@", departments];
158
b1314b7 @casademora File reorg, updating the README
casademora authored
159 NSArray *people = [Person MR_findAllWithPredicate:peopleFilter];
dc6ab2c @casademora updated readme, changed to markdown
casademora authored
160
b9e3546 @casademora Updating documentation
casademora authored
161 #### Returning an NSFetchRequest
dc6ab2c @casademora updated readme, changed to markdown
casademora authored
162
163 NSPredicate *peopleFilter = [NSPredicate predicateWithFormat:@"Department IN %@", departments];
164
b1314b7 @casademora File reorg, updating the README
casademora authored
165 NSArray *people = [Person MR_fetchAllWithPredicate:peopleFilter];
dc6ab2c @casademora updated readme, changed to markdown
casademora authored
166
167 For each of these single line calls, the full stack of NSFetchRequest, NSSortDescriptors and a simple default error handling scheme (ie. logging to the console) is created.
168
b9e3546 @casademora Updating documentation
casademora authored
169 #### Customizing the Request
dc6ab2c @casademora updated readme, changed to markdown
casademora authored
170
171 NSPredicate *peopleFilter = [NSPredicate predicateWithFormat:@"Department IN %@", departments];
172
b1314b7 @casademora File reorg, updating the README
casademora authored
173 NSFetchRequest *peopleRequest = [Person MR_requestAllWithPredicate:peopleFilter];
dc6ab2c @casademora updated readme, changed to markdown
casademora authored
174 [peopleRequest setReturnsDistinctResults:NO];
175 [peopleRequest setReturnPropertiesNamed:[NSArray arrayWithObjects:@"FirstName", @"LastName", nil]];
176 ...
177
b1314b7 @casademora File reorg, updating the README
casademora authored
178 NSArray *people = [Person MR_executeFetchRequest:peopleRequest];
dc6ab2c @casademora updated readme, changed to markdown
casademora authored
179
180 #### Find the number of entities
181
182 You can also perform a count of entities in your Store, that will be performed on the Store
183
b1314b7 @casademora File reorg, updating the README
casademora authored
184 NSNumber *count = [Person MR_numberOfEntities];
dc6ab2c @casademora updated readme, changed to markdown
casademora authored
185
186 Or, if you're looking for a count of entities based on a predicate or some filter:
187
b1314b7 @casademora File reorg, updating the README
casademora authored
188 NSNumber *count = [Person MR_numberOfEntitiesWithPredicate:...];
b9e3546 @casademora Updating documentation
casademora authored
189
190 There are also counterpart methods which return NSUInteger rather than NSNumbers:
191
192 * countOfEntities
193 * countOfEntitiesWithContext:(NSManagedObjectContext *)
194 * countOfEntitiesWithPredicate:(NSPredicate *)
195 * countOfEntitiesWithPredicate:(NSPredicate *) inContext:(NSManagedObjectContext *)
dc6ab2c @casademora updated readme, changed to markdown
casademora authored
196
26d74ec @casademora Adding aggregate operation support submitted from Duane Fields. Thanks f...
casademora authored
197 #### Aggregate Operations
198
199 NSPredicate *prediate = [NSPredicate predicateWithFormat:@"diaryEntry.date == %@", today];
b1314b7 @casademora File reorg, updating the README
casademora authored
200 int totalFat = [[CTFoodDiaryEntry MR_aggregateOperation:@"sum:" onAttribute:@"fatColories" withPredicate:predicate] intValue];
201 int fattest = [[CTFoodDiaryEntry MR_aggregateOperation:@"max:" onAttribute:@"fatColories" withPredicate:predicate] intValue];
26d74ec @casademora Adding aggregate operation support submitted from Duane Fields. Thanks f...
casademora authored
202
dc6ab2c @casademora updated readme, changed to markdown
casademora authored
203 #### Finding from a different context
204
205 All find, fetch and request methods have an inContext: method parameter
206
207 NSManagedObjectContext *someOtherContext = ...;
208
b1314b7 @casademora File reorg, updating the README
casademora authored
209 NSArray *peopleFromAnotherContext = [Person MR_findAllInContext:someOtherContext];
dc6ab2c @casademora updated readme, changed to markdown
casademora authored
210
211 ...
212
b1314b7 @casademora File reorg, updating the README
casademora authored
213 Person *personFromContext = [Person MR_findFirstByAttribute:@"lastName" withValue:@"Gump" inContext:someOtherContext];
dc6ab2c @casademora updated readme, changed to markdown
casademora authored
214
215 ...
216
b1314b7 @casademora File reorg, updating the README
casademora authored
217 NSUInteger count = [Person MR_numberOfEntitiesWithContext:someOtherContext];
dc6ab2c @casademora updated readme, changed to markdown
casademora authored
218
219
220 ## Creating new Entities
221
222 When you need to create a new instance of an Entity, use:
223
b1314b7 @casademora File reorg, updating the README
casademora authored
224 Person *myNewPersonInstance = [Person MR_createEntity];
dc6ab2c @casademora updated readme, changed to markdown
casademora authored
225
226 or, to specify a context:
227
228 NSManagedObjectContext *otherContext = ...;
229
b1314b7 @casademora File reorg, updating the README
casademora authored
230 Person *myPerson = [Person MR_createInContext:otherContext];
dc6ab2c @casademora updated readme, changed to markdown
casademora authored
231
232
233 ## Deleting Entities
234
235 To delete a single entity:
236
237 Person *p = ...;
b1314b7 @casademora File reorg, updating the README
casademora authored
238 [p MR_deleteEntity];
dc6ab2c @casademora updated readme, changed to markdown
casademora authored
239
240 or, to specify a context:
241
242 NSManagedObjectContext *otherContext = ...;
243 Person *deleteMe = ...;
244
b1314b7 @casademora File reorg, updating the README
casademora authored
245 [deleteMe MR_deleteInContext:otherContext];
dc6ab2c @casademora updated readme, changed to markdown
casademora authored
246
247 There is no delete *All Entities* or *truncate* operation in core data, so one is provided for you with Active Record for Core Data:
248
b1314b7 @casademora File reorg, updating the README
casademora authored
249 [Person MR_truncateAll];
dc6ab2c @casademora updated readme, changed to markdown
casademora authored
250
251 or, with a specific context:
252
253 NSManagedObjectContext *otherContext = ...;
b1314b7 @casademora File reorg, updating the README
casademora authored
254 [Person MR_truncateAllInContext:otherContext];
dc6ab2c @casademora updated readme, changed to markdown
casademora authored
255
256 ## Performing Core Data operations on Threads
257
468b2d1 @casademora Updating threading instructions
casademora authored
258 MagicalRecord also provides some handy methods to set up background context for use with threading. The background saving operations are inspired by the UIView animation block methods, with few minor differences:
dc6ab2c @casademora updated readme, changed to markdown
casademora authored
259
468b2d1 @casademora Updating threading instructions
casademora authored
260 * The block in which you add your data saving code will never be on the main thread.
261 * a single NSManagedObjectContext is provided for your operations.
dc6ab2c @casademora updated readme, changed to markdown
casademora authored
262
468b2d1 @casademora Updating threading instructions
casademora authored
263 For example, if we have Person entity, and we need to set the firstName and lastName fields, this is how you would use MagicalRecord to setup a background context for your use:
dc6ab2c @casademora updated readme, changed to markdown
casademora authored
264
cce797d @casademora Update threading instructions
casademora authored
265 Person *person = ...;
468b2d1 @casademora Updating threading instructions
casademora authored
266 [MagicalRecord saveInBackgroundWithBlock:^(NSManagedObjectContext *localContext){
267
268 Person *localPerson = [person MR_inContext:localContext];
dc6ab2c @casademora updated readme, changed to markdown
casademora authored
269
468b2d1 @casademora Updating threading instructions
casademora authored
270 localPerson.firstName = @"John";
271 localPerson.lastName = @"Appleseed";
272
dc6ab2c @casademora updated readme, changed to markdown
casademora authored
273 }];
274
468b2d1 @casademora Updating threading instructions
casademora authored
275 In this method, the specified block provides you with the proper context in which to perform your operations, you don't need to worry about setting up the context so that it tells the Default Context that it's done, and should update because changes were performed on another thread.
cce797d @casademora Update threading instructions
casademora authored
276
5470cc6 @casademora Made the default managed object context section more clear.
casademora authored
277 To perform an action after this save block is completed, you can fill in a completion block:
278
279 Person *person = ...;
468b2d1 @casademora Updating threading instructions
casademora authored
280 [MagicalRecord saveInBackgroundWithBlock:^(NSManagedObjectContext *localContext){
281
282 Person *localPerson = [person MR_inContext:localContext];
5470cc6 @casademora Made the default managed object context section more clear.
casademora authored
283
468b2d1 @casademora Updating threading instructions
casademora authored
284 localPerson.firstName = @"John";
285 localPerson.lastName = @"Appleseed";
286
5470cc6 @casademora Made the default managed object context section more clear.
casademora authored
287 } completion:^{
288
289 self.everyoneInTheDepartment = [Person findAll];
468b2d1 @casademora Updating threading instructions
casademora authored
290
5470cc6 @casademora Made the default managed object context section more clear.
casademora authored
291 }];
292
293 This completion block is called on the main thread (queue), so this is also safe for triggering UI updates.
294
d66b419 @casademora Updated some docs
casademora authored
295 # Data Import
296
2bd21c4 @casademora Add logging information
casademora authored
297 MagicalRecord will now import data from NSObjects into your Core Data store. [Documentation](https://github.com/magicalpanda/MagicalRecord/wiki/Data-Import) for this feature is forthcoming.
dc6ab2c @casademora updated readme, changed to markdown
casademora authored
298
299 # Extra Bits
38515e7 @casademora Minor Readme Updates
casademora authored
300 This Code is released under the MIT License by [Magical Panda Software, LLC](http://www.magicalpanda.com). We love working on iOS and Mac apps for you!
b1314b7 @casademora File reorg, updating the README
casademora authored
301 There is no charge for Awesome.
Something went wrong with that request. Please try again.