⬆️ ⬇️

Core Data: import data with a minimum of code

Like many developers, I don’t like to write a lot of code, especially where it seems unnecessary - in the early stages I try to figure out how to optimize and generalize this code. As for Core Data itself, it always seemed to me that all these endless fetches and the creation of new objects can be simplified. Then I discovered the ActiveRecord pattern often mentioned in Habré and its very good (in my opinion) implementation on Objective-C - MagicalRecord . I will not go deeper into the description - everything is very well described on the project page.

The next step of simplification was to be the mapping of data coming from outside.



For myself, I solved this problem head on and worked for a long time. Each ManagedObject-successor contained the following method, which parses the incoming JSON dictionary:



- (void)mapPropertiesFrom:(NSDictionary *)dictonary { self.identifier = [NSNumber numberWithInt:[[dictonary objectForKey:@"id"] intValue]]; self.privacy = [NSNumber numberWithInt:[[dictonary objectForKey:@"public"] intValue]]; self.author = [KWUser findFirstByAttribute:@"profileId" withValue:[dictonary objectForKey:@"profile"]]; self.profileId = [NSNumber numberWithInt:[[dictonary objectForKey:@"profile"] intValue]]; self.latitude = [NSNumber numberWithDouble:[[dictonary objectForKey:@"lat"] doubleValue]]; self.longitude = [NSNumber numberWithDouble:[[dictonary objectForKey:@"lon"] doubleValue]]; self.text = [dictonary objectForKey:@"text"]; self.category = [dictonary objectForKey:@"category"]; self.firstName = [dictonary objectForKey:@"firstname"]; self.lastName = [dictonary objectForKey:@"lastname"]; } 


')

It is obvious that there are a lot of such objects, moreover, they also have relations. The first thought that comes to mind is to iterate over attributes and relationships in runtime and save the result to objects. Unfortunately , joy, I do not like to reinvent the wheel and recently stumbled upon a very interesting implementation of this approach.



Magical Import



MagicalImport is a set of categories that extend the functionality of MagicalRecord and allow you to customize the mapping of JSON objects directly in Core Data, while writing code is reduced to an indecent minimum. I will not delve into the details of the implementation and the goals that were set before the developers, about all this is well written here . Let us dwell on a specific example.



Data retrieval



Take for example a simple request to the Forsquare API, which will return us a list of objects near the Eiffel Tower.



Request URL:

api.foursquare.com/v2/venues/search?v=20120602&ll=48.858%2C2.2944&client_secret=ILG5POWGBRBZDXLNPAGECAZOBC0KFPQAQ5SYOP51KFYANZ1B&client_id=HDEHROGPMARZ2O1JTK55VHXE4TTNGE0NQR4DBCKHFZULURJV>



I used AFNetworking to get responsa. API wrapper class:



 + (LDFourSquareAPIClient *)sharedClient { static LDFourSquareAPIClient *_sharedClient = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ _sharedClient = [[LDFourSquareAPIClient alloc] initWithBaseURL:[NSURL URLWithString:kBaseURL]]; }); return _sharedClient; } - (id)initWithBaseURL:(NSURL *)url { if (self = [super initWithBaseURL:url]) { [self registerHTTPOperationClass:[AFJSONRequestOperation class]]; [self setDefaultHeader:@"Accept" value:@"application/json"]; } return self; } 




Form the request parameters



  NSString *latLon = @"48.858,2.2944"; NSString *clientID = [NSString stringWithUTF8String:kCLIENTID]; NSString *clientSecret = [NSString stringWithUTF8String:kCLIENTSECRET]; NSDictionary *queryParams; queryParams = [NSDictionary dictionaryWithObjectsAndKeys:latLon, @"ll", clientID, @"client_id", clientSecret, @"client_secret", @"20120602", @"v", nil]; 




kCLIENTID and kCLIENTSECRET are keys for authorization. You can register your application, you can use mine.



The request to the server and data retrieval looks like this:



  [[LDFourSquareAPIClient sharedClient] getPath:@"v2/venues/search" parameters:queryParams success:^(AFHTTPRequestOperation *operation, id responseObject) { NSArray *venues = [[responseObject objectForKey:@"response"] objectForKey:@"venues"]; } failure:^(AFHTTPRequestOperation *operation, NSError *error) { }]; 




The venues array will contain a list of places that we will store in our data model. JSON object venue:



 categories = ( { icon = { name = ".png"; prefix = "https://foursquare.com/img/categories/building/default_"; sizes = ( 32, 44, 64, 88, 256 ); }; id = 4bf58dd8d48988d12d941735; name = "Monument / Landmark"; pluralName = "Monuments / Landmarks"; primary = 1; shortName = Landmark; } ); contact = { formattedPhone = "+33 892 70 12 39"; phone = "+33892701239"; }; hereNow = { count = 3; groups = ( { count = 3; items = ( ); name = "Other people here"; type = others; } ); }; id = 4adcda09f964a520dd3321e3; likes = { count = 0; groups = ( ); }; location = { address = "Parc du Champ de Mars"; city = Paris; country = France; crossStreet = "5 av. Anatole France"; distance = 42; lat = "48.85836229464931"; lng = "2.2945761680603027"; postalCode = 75007; state = "\U00cele de France"; }; name = "Tour Eiffel"; specials = { count = 0; items = ( ); }; stats = { checkinsCount = 31211; tipCount = 430; usersCount = 22307; }; url = "http://www.tour-eiffel.fr"; verified = 0; } 




Data model



The data model in this example consists of two objects — Venue and Location and the one-to-one relationship between them.











Data import



If the JSON response of the service is “perfect” and its model is fully consistent with our base model, then in order to make the import, you need to add the following code in the completion block:



 self.data = [Venue MR_importFromArray:venues]; 


to import the entire data array, or



 Venue *myVenue = [Venue MR_importFromObject:[venues objectAtIndex:0]]; 


to create a single object.



At once I ’ll make a reservation that MR_importFromArray for some reason does not work (a ticket on github) so I used the following code for import:



 NSMutableArray *arr = [NSMutableArray array]; for (NSDictionary *venueDict in venues) { [arr addObject:[Venue MR_importFromObject:venueDict]]; } self.data = arr; 




Unfortunately, server responses do not always please us with their accuracy and the name of objects and relationships often do not correspond to the model. This is where the UserInfo dictionary comes in, which each entity has, an attribute or a relationship. It allows you to configure mapping for each of these objects.



To reconfigure the attribute mapping, you need to add to this dictionary the pair 'mappedKeyName' - 'attribute name from JSON':







Also, this mapping supports KVC, which is very useful if there is no desire to create nested entities in the model (we get rid of the Stats entity, we get access to the number of chekins):







Each object in the model should have something like primaryKey: MagicalImport will look for an attribute named objectNameID, or we can specify such an attribute in UserInfo (for example, the relationship between Location and Venue):







I apologize for using lat as the 'primary key', naturally this was done just for the sake of example.



Import callbacks



The import mechanism provides callbacks that can be used to check / edit the processed data (implemented inside the NSManagedObject subclasses):





For example, we will implement a check, based on which it will establish relationships only with those Location objects that contain the address:

 - (BOOL)shouldImportLocation:(id)location { NSString *address = [location objectForKey:@"address"]; return address ? YES : NO; } 




Conclusion



The considered example is trivial and therefore does not allow you to familiarize yourself with all the subtleties of MagicalImport (as well as the fact that there is no documentation on it yet), but it seems to me that it allows you to feel the advantages for which he thought: no extra code and flexibility when importing data.

Test project can be found here . (CocoaPods was used to connect MagicalRecord and AFNetworking ).

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



All Articles