📜 ⬆️ ⬇️

Using MagicalRecord when developing iOS applications

Good day, good day!

Today I want to present to your attention another translation, do not judge strictly :) I hope you find this material useful in your work.

For years, Core Data has been an integral part of many OS X and iOS applications, providing storage and requesting user data. Apple spends a lot of effort to make Core Data API easier to use and easier for developers to integrate into applications.
')
This fact indicates that Core Data is a highly modifiable project. Even if you know how to use Data Core, performing simple, daily tasks may seem like a difficult and voluminous job. It’s good that MagicalRecord exists - an independent Core Data library created by MagicalPanda. And this tutorial will teach you how to speed up work with MagicalRecord quickly and easily.

MagicalRecord is easy to use, well designed and popular. The authors of the project stated that the main task of MagicalRecord is to clean up the code you need to write to use Core Data and, with one simple line of code, sample the data while simultaneously allowing the user to optimize performance. “How is this possible?” - you think. This is possible thanks to a convenient technology that uses the same template for customizing, querying and updating Core Data. The design feature is the influence of Ruby on Rails' ActiveRecord storage system.



Enough theory! Follow the instructions and you will see how MagicalRecord works. This article describes how to create an application that will track what your favorite beer is. This will allow you to:

Add beer to taste.



Let's start

To fully understand this article, you should already have a basic understanding of CoreData, at the level of the CoreData study guide. In addition, you do not need any prior experience with CoreData.

If you don’t even have a basic Core Data experience, I recommend that you first familiarize yourself with this Core Data tutorial and then come back here.

To get started, download this starter project. Once it is loaded, unpack the archive and run the application to check it.

image


The application has a basic navigation controller with the “Add” button, as well as a table, a search line (if you pull down an empty table) and a segment choice for sorting alphabetically or by rating. If you click the Add button, you will be taken to the interface where you will need to enter the name of the beer and view information about it. If you try to enter something, then until it will not be saved.

Now look at the code. In Project Navigator, you will see:



Looking through, you may notice that the CoreData model is missing, and AppDelegate.m does not contain any code for installation. This is the perfect scenario for any project that you started without Core Data. But the understanding that without Core Data - anywhere, will come only later.

Meet MagicalRecord

In the Project Navigator, select and expand the MagicalRecord group. Inside you will see the Categories and Core groups, as well as CoreData + MagicalRecord.h. Expand the Categories group and open a file called NSManagedObjectModel + MagicalRecord.h.

You will notice that all names begin with the prefix MR_. In fact, if you go through any files in the Categories group, you will notice that all names that begin with the prefix MR_.

I do not know about you, but it seems to me rather strange to begin each name with the same letters. It’s good that MagicalRecord makes it easy to change them using Shorthand Support.

Again, in the Project Navigator, expand the Supporting Files group, and open the BeerTracker-Prefix.pch file. This is a precompiled header for the project. Add two lines of code to the “start project” to this file:

#define MR_SHORTHAND #import “CoreData+MagicalRecord.h” 


These two lines activate MagicalRecord for your project:

  1. MR_SHORTHAND tells MagicalRecord that you do not want to enter MR_ before each method with MagicalRecord. By logging into MagicalRecord + ShorthandSupport.m you can learn more about how it works. But it is beyond the scope of this article, and therefore will not be discussed here :)
  2. By importing CoreData + MagicalRecord.h, you can now access any of the MagicalRecord APIs in the project files, and you no longer have to import them again and again into each file.


Note: If you want to add MagicalRecord to your project, you can use additional tips on their GitHub page.

You can also add MagicalRecord in the same way as I added it to the project:

  1. Download the MagicalRecord project from GitHub
  2. Drag and drop MagicalRecord (containing CoreData + MagicalRecord.h, Categories, and Core) into your Xcode project
  3. Make sure that the CoreData database is added to the project settings in Build Phases \ Link Binary with Libraries
  4. In your precompiled header, add the following #ifdef __OBJC__:

 #define MR_SHORTHAND #import “CoreData+MagicalRecord.h” 


Model "Beer"

To start tracking your favorite varieties, you need to create a model for this, because you can hardly remember them all, right? In the Xcode menu, select File \ New \ File ... from the list on the left, select Core Data and select - Data Model.

image

Name the file BeerModel.xcdatamodeld and move it to the BeerTracker group.

In the project navigator, select the new BeerModel.xcdatamodeld bundle and start editing it. Add an object and name it "Beer". Add property - name of type String.

image

Add another object called BeerDetails. This object will monitor such indicators as: user rating, notes and where to find the image. Add the following BeerDetails properties of the appropriate types:



image

Next, create a connection between the two objects, so that the beer object must know that BeerDetails belongs to it. In BeerDetails, create a new relationship and name it “beer” with a target beer. By choosing a beer destination, you created the first part of the relationship between subjects.

image

Complete the relationship setup by selecting a Beer object. Add a relationship with the name beerDetails, with the purpose of BeerDetails and invert Beer. Now your Beer object will have a BeerDetails object to distinguish the same sorts.

image

The next step is to classify data to represent objects. To do this, use Xcode, but you have to be careful, because Xcode is sometimes buggy.

First, change the CoreData model by selecting it in the Xcode project navigator; make sure you select the Beer object in the “objects” panel and not among the BeerDetails objects.

Next, in the Xcode menu, go to Editor \ Create NSManagedObject Subclass ... check BeerModel, then click Next. Under the control subjects, check the boxes for Beer and BeerDetails, if they are not set up by default, and double check that Beer is highlighted. Click “Next” and then “Create”. This will create new classes, Beer and BeerDetails with corresponding objects and names. Look at the new classes, and you will see that each class has properties that match the properties of the objects you defined.

Check again the properties that represent the relationship between entities. You should see that the Beer class contains a beerDetails property of type BeerDetails * object. But, you will also see that the BeerDetails class contains the NSManagedObject * beer feature. Why properties do not have beer type *? This is due to limitations in the "Xcode Create NSManagedObject Subclass" command. It does not affect this project, so just ignore it.

Note: Tried curiosity? I wonder why Xcode does not create cdjqcndf * Beer? This seems to be the result of the restrictions in the CreateNSManagedObjectSubclass Xcode command. Since Xcode recognizes the CoreData Model, it is logical to assume that the team should be able to conclude that the properties of each class must have a mutual bond with each other.

However, this command not only generates properties of different types of classes. It also generates the names of the classes used to define these types, and the command is not correct enough to fully perform both tasks. First, you can simply generate the code and set the exact type and subclass property (adding a description as necessary). Another way is to reduce the number of tasks for Xcode. If you clearly defined the class name for each object in the Data Model Inspector before you create the classes themselves, then Xcode will create the correct type properties.

If you are interested in more complex tools for creating classes from Xcode data models, one of them is mogenerator .

Now that you have created classes for data objects, it's time to initialize the Core Data stack. Open the AppDelegate.m file, and add the following lines of code before the return statement to the application: didFinishLaunchingWithOptions: delegate method:

 // Setup CoreData with MagicalRecord // Step 1. Setup Core Data Stack with Magical Record // Step 2. Relax. Why not have a beer? Surely all this talk of beer is making you thirsty… [MagicalRecord setupCoreDataStackWithStoreNamed:@"BeerModel"]; 


If you’ve worked with a Core Data Xcode project, you’ve probably seen how many codes are required to connect Core Data and initialize it in the AppDelegate file. With MagicalRecord, all you need is just one line of code!

MagicalRecord provides several alternative ways to customize the Core Data stack, depending on the following:



If your Model file has the same base name as the project name (for example, the BeerTracker.xcdatamodeld model file, as part of a project called BeerTracker), then you can use MagicalRecord in one of three convenient ways -setupCoreDataStack, setupCoreDataStackWithInMemoryStore, or setupAutoMiggrade- restratoratemore antigramdratemtagratyrate, 3DChartCrateDoreStack, you can use setupCoreDataStackWithInMemoryStore, setupCoreDataStackWithInMemoryStore, setupCoreDataStackWithInMemoryStore, setupCoreDataStackWithInMemoryStore.

But, since your models are called differently, you should use one of two other types of installation: setupCoreDataStackWithStoreNamed: or setupCoreDataStackWithAutoMigratingsqlitestorenamed:

Using the AutoMigrating configuration method, you can change your model, and if you can automatically transfer your storage, MagicalRecord will do it for you. As a rule, Core Data requires you to add code to process small changes made to the model.

Brewing (entity)

Now, your model and Core Data stack are tuned. You can start adding beer to your list. Open the BeerViewController.h file, and after the AMRatingControl class , add:

 @class Beer; 

And you can also add the properties of the beer after the interface :

 @property (nonatomic, strong) Beer *beer; 


Then go to the BeerViewController.m file and import Beer.h and BeerDetails.h:

 #import "Beer.h" #import "BeerDetails.h" 


In viewDidLoad, add the following code block:

 - (void)viewDidLoad { // 1. If there is no beer, create new Beer if (!self.beer) { self.beer = [Beer createEntity]; } // 2. If there are no beer details, create new BeerDetails if (!self.beer.beerDetails) { self.beer.beerDetails = [BeerDetails createEntity]; } // View setup // 3. Set the title, name, note field and rating of the beer self.title = self.beer.name ? self.beer.name : @"New Beer"; self.beerNameField.text = self.beer.name; self.beerNotesView.text = self.beer.beerDetails.note; self.ratingControl.rating = [self.beer.beerDetails.rating integerValue]; [self.cellOne addSubview:self.ratingControl]; // 4. If there is an image path in the details, show it. if ([self.beer.beerDetails.image length] > 0) { // Image setup NSData *imgData = [NSData dataWithContentsOfFile:[NSHomeDirectory() stringByAppendingPathComponent:self.beer.beerDetails.image]]; [self setImageForBeer:[UIImage imageWithData:imgData]]; } } 


Troubleshooting: if you have any errors after copying the previous code block into your project, and make Clean for your project, press Shift + Command + K, or go to Product \ Clean.

BeerViewController loads because:


Now that the view has loaded, you need to do the following:

  1. Check whether the Beer object is loaded. If not, it means that you need to add a new sort of beer.
  2. If Beer does not have any BeerDetails, create an object BeerDetails.
  3. To customize the view, enter the name of the beer, its rating and note the content. If this beer does not have a name yet (in the case of the introduction of a new beer, the project will give it the name “new beer”).
  4. If the beer contains the path to the image, it will load the image in the UIImageView.


There are a few things that need to be set up so that you can edit and add new beers. First, you must be able to add or change the name. Edit textFieldDidEndEditing: so that it looks like the one below:

 - (void)textFieldDidEndEditing:(UITextField *)textField { if ([textField.text length] > 0) { self.title = textField.text; self.beer.name = textField.text; } } 


Now, when you finish adding the textField name, it will set the name of the beer and the entire contents of the textField until the field is filled.

To save the contents of the note, you need to evaluate the beer, find textViewDidEndEditing :, and change the contents of the method to make it look as shown below:

 - (void)textViewDidEndEditing:(UITextView *)textView { [textView resignFirstResponder]; if ([textView.text length] > 0) { self.beer.beerDetails.note = textView.text; } } 


Next, make sure that the beer rating is updated after the user changes it in the menu: View Controller. Find the updateRating method, and add the following:

 - (void)updateRating { self.beer.beerDetails.rating = @(self.ratingControl.rating); } 


When the user clicks on the UIImageView on the “details” page, this allows him to add or edit a photo. In UIActionSheet displays, which allows the user to select an image taken with his camera, or take a new photo. If the user wants to take a picture, he must make sure that the image is saved on the disk. Instead of saving the image in CoreData (which can cause problems with the performance of the computer), save the image in the user's documents with the rest of the photos, and this path is used only for Core Data.

Controlling the interaction between the camera and the photo library is provided by implementing the UIImagePickerControllerDelegate protocol methods. You need to make BeerViewController a delegate for the UIImagePickerController, since this is a storyboard processing controller. Find the imagePickerController: didFinishPickingMediaWithinfo :, method and add the following:

 - (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info { // 1. Grab image and save to disk UIImage *image = info[UIImagePickerControllerOriginalImage]; // 2. Remove old image if present if (self.beer.beerDetails.image) { [ImageSaver deleteImageAtPath:self.beer.beerDetails.image]; } // 3. Save the image if ([ImageSaver saveImageToDisk:image andToBeer:self.beer]) { [self setImageForBeer:image]; } [picker dismissViewControllerAnimated:YES completion:nil]; } 


Here is what happens in the code above:
  1. imagePickerController: didFinishPickingMediaWithInfo: indirectly transfers the link to the image selected by the user, where the image itself is already in the info dictionary, under the UIImagePickerControllerOriginalImage key ...
  2. If the Beer object already contains an image, the application will delete it from the disk, so that the Account’s storage needlessly is not overfilled.
  3. And then a new image is saved to disk and its path is added to BeerDetails in the image properties. Open ImageSaver and search for saveImageToDisk: andToBeer to see what it looks like. After the image is successfully saved, it is displayed in the UIImageView.
  4. After that, the picker is hidden.


You will need to slightly change the ImageSaver to get started. Now that the Beer class has been created, you can not comment on the import line, and the line that sets the path to the image object. Open the ImageSaver.m file and change the import instructions:

 #import "ImageSaver.h" #import "Beer.h" #import "BeerDetails.h" 


Now you need to uncomment the line with the if instruction.

 if ([imgData writeToFile:jpgPath atomically:YES]) { beer.beerDetails.image = path; } 


The ImageSaver class is now fully prepared to take an image and save it on your phone, documents, directories, and paths in the BeerDetails object.

Based on this, the user has two options: cancel or add a new beer. When creating the view, a Beer object was created, and inserted into the managedObjectContext. Cancel must delete the Beer object. Find the cancelAdd method, and add the following:

 - (void)cancelAdd { [self.beer deleteEntity]; [self.navigationController popViewControllerAnimated:YES]; } 


MagicalRecord provides a good way to delete an object that automatically deletes objects from the ManagedObjectContext. After deletion, the user will return to the main beer list.

If the user clicks the Done button, this will save the beer and return to the main list. Find the addNewBeer method. It simply displays the view controller and returns to the list. When the view closes, the viewWillDisapper: method calls. This in turn will make the callssaveContext call.

Right now, the saveContext method is empty, so you need to add some code to save the object. Add the following code to the saveContext method:

 - (void)saveContext { [[NSManagedObjectContext defaultContext] saveToPersistentStoreWithCompletion:^(BOOL success, NSError *error) { if (success) { NSLog(@"You successfully saved your context."); } else if (error) { NSLog(@"Error saving context: %@", error.description); } }]; } 


There are only a few lines of code! In AppDelegate.m, you configure the core data of the WithMagicalRecord stack. This created access to the default application with a managedObjectContext. When creating Beer and BeerDetails objects, they were added to defaultContext. MagicalRecord allows you to save a managedObjectContext using saveToPersistentStoreWithCompletion :. Completing a block gives you access to an NSError object, in case you were unable to save. Here, you added a simple if / else block that registers to what happens after you save the defaultContext.

Are you ready to check your creation? Now, launch your app! Click the "+" button, fill in any information you want. Then click finish.

image

After you do this, you will notice that the new beer is not displayed in the list. Do not worry, it is not broken, and you will learn how to fix it all later. You may also have noticed that the debugger has a whole bunch of information. Contrary to what your logic can say, this is really good.

image

Magic debugger

When the application is running, MagicalRecord enters four things when entering the Core Data installation stack. This indicates that the Core Data setting has occurred, and that the defaultContext has been created. The same defaultContext that was mentioned above when you saved the Beer object.

After you click the Done button and save the MagicalRecord several more protocols appear. In the process of preservation, the following protocols were produced:

  1. The default thread is stored in defaultContext.
  2. All source materials will be saved and flagged 1.
  3. Synchronization will not take place simultaneously and all saved files will be marked with a flag 0.
  4. The final logs show that MagicalRecord knows about the two objects that should be saved (Beer and BeerDetails objects), and that they have been successfully saved.


MagicalRecord logs a large amount of information. If you have problems, or something is not working properly, you should check your protocols, because They carry a lot of useful information.

Note:
Although I do not recommend it, but if you really do not want to see what happens when the MagicalRecord protocols, go to MagicalRecord.h and replace line 17:

 #define MR_ENABLE_ACTIVE_RECORD_LOGGING 1 

on
 #define MR_ENABLE_ACTIVE_RECORD_LOGGING 0 


Another beer please!

If this app is worth all the effort, you can browse all the beers you added. Open MasterViewController.m, and import Beer.h, and BeerDetails.h.

In order to get all the beers stored in CoreData, you will need to make a selection. Add a selection to viewWillAppear: before calling the reloadData method.

 - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; // Check if the user's sort preference has been saved. ... [self fetchAllBeers]; [self.tableView reloadData]; } 


When you first view downloads, or when you return after viewing or adding a beer, you should load all types of beer in the table.

Find the fetchAllBeers method, and add the following:

 - (void)fetchAllBeers { // 1. Get the sort key NSString *sortKey = [[NSUserDefaults standardUserDefaults] objectForKey:WB_SORT_KEY]; // 2. Determine if it is ascending BOOL ascending = [sortKey isEqualToString:SORT_KEY_RATING] ? NO : YES; // 3. Fetch entities with MagicalRecord self.beers = [[Beer findAllSortedBy:sortKey ascending:ascending] mutableCopy]; } 


MasterViewController allows the user to sort beer by rating with a rating from 5 to 1 per beer, or in alphabetical order (AZ). The first time the application is launched, it creates NSUserDefault (sorting by rating) and sets it as default. In this case, you:

  1. The recovered sort key is stored in NSUserDefaults
  2. If the sort key is “rating”, then the ascending variable value should be noted as “no”. If alphabetically, the value is "yes."
  3. Running fetch.


Yes, that's really all you need to do!

image

Once again, you use MagicalRecord as a way to interact with Core Data. findAllSortedBy: ascending is just one of many ways to sample the core data of subjects using MagicalRecord. Some others include (note, you'll need to use one of them later):



There are many other things you can use - just check out NSManagedObject + MagicalFinders.m.

To display the beer name and rating in the cell, follow configureCell: atIndex :, and add the following:

 - (void)configureCell:(UITableViewCell*)cell atIndex:(NSIndexPath*)indexPath { // Get current Beer Beer *beer = self.beers[indexPath.row]; cell.textLabel.text = beer.name; // Setup AMRatingControl AMRatingControl *ratingControl; if (![cell viewWithTag:20]) { ratingControl = [[AMRatingControl alloc] initWithLocation:CGPointMake(190, 10) emptyImage:[UIImage imageNamed:@"beermug-empty"] solidImage:[UIImage imageNamed:@"beermug-full"] andMaxRating:5]; ratingControl.tag = 20; ratingControl.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin; ratingControl.userInteractionEnabled = NO; [cell addSubview:ratingControl]; } else { ratingControl = (AMRatingControl*)[cell viewWithTag:20]; } // Put beer rating in cell ratingControl.rating = [beer.beerDetails.rating integerValue]; } 


Now find the prepareForSegue: sender: method, and if that checks if the transition is the identifier “editBeer,” add:

 if ([[segue identifier] isEqualToString:@"editBeer"]) { NSIndexPath *indexPath = [self.tableView indexPathForSelectedRow]; Beer *beer = self.beers[indexPath.row]; upcoming.beer = beer; } 


This is to transfer the Beer object to BeerViewController so that it displays information about the beer, allowing you to edit it.

Try running the project again.

Now you will see the beer that was added earlier, with its rating. Also, you can choose a beer and edit information. When you return, the table will be updated! Very good!

image

Try viewing the Beer object separately, and without editing the information, return to the main list. Take a look at the magazine. You should see:

 -[NSManagedObjectContext(MagicalSaves) MR_saveWithOptions:completion:](0x8b6bfa0) NO CHANGES IN ** DEFAULT ** CONTEXT - NOT SAVING 


, , , viewWillDisappear:. , , MagicalRecord , , . , , — , MagicalRecord .



, , , , — , .

Deletion

MasterViewController.m, tableView:commitEditingStyle:forRowAtIndexPath:, :

 - (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath { if (editingStyle == UITableViewCellEditingStyleDelete) { Beer *beerToRemove = self.beers[indexPath.row]; // Remove Image from local documents if (beerToRemove.beerDetails.image) { [ImageSaver deleteImageAtPath:beerToRemove.beerDetails.image]; } // Deleting an Entity with MagicalRecord [beerToRemove deleteEntity]; [self saveContext]; [self.beers removeObjectAtIndex:indexPath.row]; [tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade]; } } 


, saveContext. , . — , ? ......? !

saveContext:

 // Save ManagedObjectContext using MagicalRecord [[NSManagedObjectContext defaultContext] saveToPersistentStoreAndWait]; 


, , MagicalRecord managedObjectContext.

( ). , , - ! , , MagicalRecord .

!

-

, , . AppDelegate.m, Beer.h, BeerDetails.h. , Core Data , :

 // Setup App with prefilled Beer items. if (![[NSUserDefaults standardUserDefaults] objectForKey:@"MR_HasPrefilledBeers"]) { // Create Blond Ale Beer *blondAle = [Beer createEntity]; blondAle.name = @"Blond Ale"; blondAle.beerDetails = [BeerDetails createEntity]; blondAle.beerDetails.rating = @4; [ImageSaver saveImageToDisk:[UIImage imageNamed:@"blond.jpg"] andToBeer:blondAle]; // Create Wheat Beer Beer *wheatBeer = [Beer createEntity]; wheatBeer.name = @"Wheat Beer"; wheatBeer.beerDetails = [BeerDetails createEntity]; wheatBeer.beerDetails.rating = @2; [ImageSaver saveImageToDisk:[UIImage imageNamed:@"wheat.jpg"] andToBeer:wheatBeer]; // Create Pale Lager Beer *paleLager = [Beer createEntity]; paleLager.name = @"Pale Lager"; paleLager.beerDetails = [BeerDetails createEntity]; paleLager.beerDetails.rating = @3; [ImageSaver saveImageToDisk:[UIImage imageNamed:@"pale.jpg"] andToBeer:paleLager]; // Create Stout Beer *stout = [Beer createEntity]; stout.name = @"Stout Lager"; stout.beerDetails = [BeerDetails createEntity]; stout.beerDetails.rating = @5; [ImageSaver saveImageToDisk:[UIImage imageNamed:@"stout.jpg"] andToBeer:stout]; // Save Managed Object Context [[NSManagedObjectContext defaultContext] saveToPersistentStoreWithCompletion:nil]; // Set User Default to prevent another preload of data on startup. [[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"MR_HasPrefilledBeers"]; [[NSUserDefaults standardUserDefaults] synchronize]; } 


, , . , . NSUserDefaults DataModel , .

, . ; . , .

image

Search

, , . — , . , , .

MagicalRecord , , . , , .
NSPredicate. NSPredicate. , ? doSearch MasterViewController.m file.

doSearch:

 - (void)doSearch { // 1. Get the text from the search bar. NSString *searchText = self.searchBar.text; // 2. Do a fetch on the beers that match Predicate criteria. // In this case, if the name contains the string self.beers = [[Beer findAllSortedBy:SORT_KEY_NAME ascending:YES withPredicate:[NSPredicate predicateWithFormat:@"name contains[c] %@", searchText] inContext:[NSManagedObjectContext defaultContext]] mutableCopy]; // 3. Reload the table to show the query results. [self.tableView reloadData]; } 


, MagicalRecord.

, , . , , , . ?

image

What's next?

, MagicalRecord , MagicalRecord. ! , , , , . Enjoy!

. , - .

BeerTracker, , :

Source: https://habr.com/ru/post/236707/


All Articles