Character
object: Character { id (int, optional): The unique ID of the character resource., name (string, optional): The name of the character., description (string, optional): A short bio or description of the character., modified (Date, optional): The date the resource was most recently modified., resourceURI (string, optional): The canonical URL identifier for this resource., urls (Array[Url], optional): A set of public web site URLs for the resource., thumbnail (Image, optional): The representative image for this character., comics (ComicList, optional): A resource list containing comics which feature this character., stories (StoryList, optional): A resource list of stories in which this character appears., events (EventList, optional): A resource list of events in which this character appears., series (SeriesList, optional): A resource list of series in which this character appears. }
Character
entity that will logically correspond (albeit partially) to our remote object.charID
, will serve to store the “native Marvel's” id
for the future, the second, the innerID
, will be needed for local use. The charDescription
and name attributes correspond to the remote description and name parameters, respectively.thumbnailImageData
and thumbnailURLString
attributes, although they do not match any parameters of the original structure. This is because in the JSON response, the thumbnail
of the Image
type and in reality corresponds to the dictionary. Here is an example of a thumbnail
object from a real answer: "thumbnail": { "path": "http://i.annihil.us/u/prod/marvel/i/mg/8/c0/4ce5a0e31f109", "extension": "jpg" }
Character
class that will inherit from NSManagedObject
. Here is his announcement: @interface Character : NSManagedObject { NSDictionary *_thumbnailDictionary; } @property (nonatomic, retain) NSString *name; @property (nonatomic, retain) NSNumber *charID; @property (nonatomic, retain) NSNumber *innerID; @property (nonatomic, retain) NSString *charDescription; @property (nonatomic, retain) NSData *thumbnailImageData; @property (nonatomic, retain) NSString *thumbnailURLString; @property NSDictionary *thumbnailDictionary; // + (NSInteger)allCharsCountWithContext:(NSManagedObjectContext *)managedObjectContext; // innerID. + (Character *)charWithManagedObjectContext:(NSManagedObjectContext *)context andInnerID:(NSInteger)charInnerID; @end
thumbnailDictionary
appeared, which I added for more convenient work with the object thumbnail, which I wrote about a little higher. I also added two auxiliary class methods to not create additional classes in the project.GDMarvelRKObjectManager
(inherited from NSObject
), which will work with RK, in particular with classes such as RKObjectManager
and RKManagedObjectStore
. This class can also not be created, however we will go for it to slightly unload the code in our future main view controller.RKManagedObjectStore
encapsulates all work with Core Data, so that in the future there will be no need to work with NSManagedObjectContext
or NSManagedObjectModel
directly. RKObjectManager
provides a centralized interface for sending requests and receiving responses using object mapping (matching). For example, the necessary values ​​received in the JSON response will be automatically assigned to all properties of our object when the mapping is successful. Isn't that what we wanted at the beginning of the article?#import <RestKit/RestKit.h>
in your * .h file. @implementation GDMarvelRKObjectManager { RKObjectManager *objectManager; RKManagedObjectStore *managedObjectStore; }
- (id)init
add the initialization of the necessary RK objects to the - (id)init
method: // AFNetworking HTTPClient NSURL *baseURL = [NSURL URLWithString:@"http://gateway.marvel.com/"]; AFHTTPClient *client = [[AFHTTPClient alloc] initWithBaseURL:baseURL]; // RKObjectManager objectManager = [[RKObjectManager alloc] initWithHTTPClient:client];
- (void)configureWithManagedObjectModel:(NSManagedObjectModel *)managedObjectModel { if (!managedObjectModel) return; managedObjectStore = [[RKManagedObjectStore alloc] initWithManagedObjectModel:managedObjectModel]; NSError *error; if (!RKEnsureDirectoryExistsAtPath(RKApplicationDataDirectory(), &error)) RKLogError(@"Failed to create Application Data Directory at path '%@': %@", RKApplicationDataDirectory(), error); NSString *path = [RKApplicationDataDirectory() stringByAppendingPathComponent:@"RKMarvel.sqlite"]; if (![managedObjectStore addSQLitePersistentStoreAtPath:path fromSeedDatabaseAtPath:nil withConfiguration:nil options:nil error:&error]) RKLogError(@"Failed adding persistent store at path '%@': %@", path, error); [managedObjectStore createManagedObjectContexts]; objectManager.managedObjectStore = managedObjectStore; }
objectManager
and managedObjectStore
.GDMarvelRKObjectManager
class: adding a mapping (correspondence) between the Core Data entity and a remote object, as well as receiving these objects from a remote server. - (void)addMappingForEntityForName:(NSString *)entityName andAttributeMappingsFromDictionary:(NSDictionary *)attributeMappings andIdentificationAttributes:(NSArray *)ids andPathPattern:(NSString *)pathPattern { if (!managedObjectStore) return; RKEntityMapping *objectMapping = [RKEntityMapping mappingForEntityForName:entityName inManagedObjectStore:managedObjectStore]; // , . [objectMapping addAttributeMappingsFromDictionary:attributeMappings]; // , . , . objectMapping.identificationAttributes = ids; // , . RKResponseDescriptor *characterResponseDescriptor = [RKResponseDescriptor responseDescriptorWithMapping:objectMapping method:RKRequestMethodGET pathPattern:[NSString stringWithFormat:@"%@%@", MARVEL_API_PATH_PATTERN, pathPattern] keyPath:@"data.results" statusCodes:[NSIndexSet indexSetWithIndex:200]]; [objectManager addResponseDescriptor:characterResponseDescriptor]; }
responseDescriptorWithMapping:...
method responseDescriptorWithMapping:...
First, the pathPattern
parameter. It is obtained by concatenating the MARVEL_API_PATH_PATTERN
macro (with the value of @"v1/public/"
) and the input parameter pathPattern
, which in our example will be equal to @"characters"
. If we want to get not a list of characters, but, say, a list of comics, then we will transmit the string @”comics”
, which is already in the body of the method again connected with @"v1/public/"
.@"data.results"
parameter for the keyPath
parameter. Where did it come from? It's very simple: Marvel wraps all its answers in the same type of wrapper, and everything will fall into place when we look at its structure: { "code": "int", "status": "string", "copyright": "string", "attributionText": "string", "attributionHTML": "string", "data": { "offset": "int", "limit": "int", "total": "int", "count": "int", "results": [ { "id": "int", "name": "string", "description": "string", "modified": "Date", "resourceURI": "string", "urls": [ { "type": "string", "url": "string" } ], "thumbnail": { "path": "string", "extension": "string" }, "comics": { "available": "int", "returned": "int", "collectionURI": "string", "items": [ { "resourceURI": "string", "name": "string" } ] }, "stories": { "available": "int", "returned": "int", "collectionURI": "string", "items": [ { "resourceURI": "string", "name": "string", "type": "string" } ] }, "events": { "available": "int", "returned": "int", "collectionURI": "string", "items": [ { "resourceURI": "string", "name": "string" } ] }, "series": { "available": "int", "returned": "int", "collectionURI": "string", "items": [ { "resourceURI": "string", "name": "string" } ] } } ] }, "etag": "string" }
@"data.results"
just indicates the path that you need to "go down".getMarvelObjectsAtPath
, which essentially proxies a call to an getObjectsAtPath
object of type RKObjectManager
. The name of the method is “speaking” - you are waiting for it to load remote objects. Since Marvel requires that hash, timestamp and public key be sent to each request, it is convenient to encapsulate the generation of these parameters in our getMarvelObjectsAtPath
. Here he is: - (void)getMarvelObjectsAtPath:(NSString *)path parameters:(NSDictionary *)params success:(void (^)(RKObjectRequestOperation *operation, RKMappingResult *mappingResult))success failure:(void (^)(RKObjectRequestOperation *operation, NSError *error))failure { // NSDateFormatter *formatter = [[NSDateFormatter alloc] init]; [formatter setDateFormat:@"yyyyMMddHHmmss"]; NSString *timeStampString = [formatter stringFromDate:[NSDate date]]; NSString *hash = [[[NSString stringWithFormat:@"%@%@%@", timeStampString, MARVEL_PRIVATE_KEY, MARVEL_PUBLIC_KEY] MD5String] lowercaseString]; NSMutableDictionary *queryParams = [NSMutableDictionary dictionaryWithDictionary:@{@"apikey" : MARVEL_PUBLIC_KEY, @"ts" : timeStampString, @"hash" : hash}]; if (params) [queryParams addEntriesFromDictionary:params]; // objectManager [objectManager getObjectsAtPath:[NSString stringWithFormat:@"%@%@", MARVEL_API_PATH_PATTERN, path] parameters:queryParams success:success failure:failure]; }
NSString
- MD5String
. How to generate MD5-troku from the line, look on the Internet .- (NSManagedObjectContext *)managedObjectContext
, which will return the main context of the managedObjectStore
. This class will also be a singleton with the + (GDMarvelRKObjectManager *)manager
method + (GDMarvelRKObjectManager *)manager
for accessing the instance.GDBaseViewController
controller, in which we simply embed support for the animation of waiting for a response from the server with the only new method - (void)animateActivityIndicator:(BOOL)animate
. In the viewDidLoad
method, viewDidLoad
will create this indicator of the UIActivityIndicatorView
type, assign the obtained value to the instance variable UIActivityIndicatorView *activityIndicator
and add it to self.view
. - (void)animateActivityIndicator:(BOOL)animate { activityIndicator.hidden = !animate; if (animate) { [self.view bringSubviewToFront:activityIndicator]; [activityIndicator startAnimating]; } else [activityIndicator stopAnimating]; }
YES
value for a single parameter, our controller view will look like this:GDMainViewController
controller GDMainViewController
inherited from this class. Here is his announcement: @interface GDMainViewController : GDBaseViewController <UITableViewDataSource, UITableViewDelegate, UIAlertViewDelegate> { UITableView *table; NSInteger numberOfCharacters; AllAroundPullView *bottomPullView; BOOL noRequestsMade; } @end
UITableView
, in which a picture and the name of each of the characters are displayed in each cell. But they must also be downloaded, since the local database is initially empty. After the entire initialization process inherent in instantiating the UITableView
in the method - (void)viewDidLoad
, we first assign our CoreData model to RKManagedObjectStore
using our wrapper class GDMarvelRKObjectManager
: NSURL *modelURL = [[NSBundle mainBundle] URLForResource:@"Marvel" withExtension:@"momd"]; [[GDMarvelRKObjectManager manager] configureWithManagedObjectModel:[[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL]]; // Character: [[GDMarvelRKObjectManager manager] addMappingForEntityForName:@"Character" andAttributeMappingsFromDictionary:@{ @"name" : @"name", @"id" : @"charID", @"thumbnail" : @"thumbnailDictionary", @"description" : @"charDescription" } andIdentificationAttributes:@[@"charID"] andPathPattern:MARVEL_API_CHARACTERS_PATH_PATTERN];
andAttributeMappingsFromDictionary:
the dictionary is transferred, consisting of the correspondences between the names of the JSON-keys of the remote object and the properties of the class we created. As the parameter andPathPattern:
the string @"characters"
is passed (macro MARVEL_API_CHARACTERS_PATH_PATTERN
) - the name of the remote JSON object.[self loadCharacters]
. - (void)loadCharacters { numberOfCharacters = [Character allCharsCountWithContext:[[GDMarvelRKObjectManager manager] managedObjectContext]]; if (noRequestsMade && numberOfCharacters > 0) { noRequestsMade = NO; return; } [self animateActivityIndicator:YES]; noRequestsMade = NO; [[GDMarvelRKObjectManager manager] getMarvelObjectsAtPath:MARVEL_API_CHARACTERS_PATH_PATTERN parameters:@{@"offset" : @(numberOfCharacters)} success:^(RKObjectRequestOperation *operation, RKMappingResult *mappingResult) { [self animateActivityIndicator:NO]; NSInteger newInnerID = numberOfCharacters; for (Character *curCharacter in mappingResult.array) { if ([curCharacter isKindOfClass:[Character class]]) { curCharacter.innerID = @(newInnerID); newInnerID++; // ( ), , [self saveToStore]; } } numberOfCharacters = newInnerID; [table reloadData]; bottomPullView.hidden = NO; [bottomPullView finishedLoading]; } failure:^(RKObjectRequestOperation *operation, NSError *error) { [bottomPullView finishedLoading]; [[[UIAlertView alloc] initWithTitle:@"Marvel API Error" message:operation.error.localizedDescription delegate:self cancelButtonTitle:@"Cancel" otherButtonTitles:@"Retry", nil] show]; }]; }
getMarvelObjectsAtPath
wrapper getMarvelObjectsAtPath
:innerID
for each of them, save them to a local database and change the value of the total number of heroes. Then update the display of our table. The main magic here is that at this stage, the resulting objects were automatically saved in our CoreData storage — RK did it for us. (It should be noted that this applies only to those fields / properties of the object for which the mapping matches are set. Thus, in the code above, the change of the innerID
parameter innerID
be saved separately by calling [self saveToStore]
). - (void)saveToStore { NSError *saveError; if (![[[GDMarvelRKObjectManager manager] managedObjectContext] saveToPersistentStore:&saveError]) XLog(@"%@", [saveError localizedDescription]); }
bottomPullView
instance bottomPullView
. This variable stores an object of type AllAroundPullView
( pulled from GitHub ) —a useful control that helps implement Pull-To-Resfresh behavior from all sides of your UIScrollView
. We will load each successive portion of our characters, reaching the bottom edge of the table and pulling it up.- (void)viewDidLoad
this control was initialized and used as follows: bottomPullView = [[AllAroundPullView alloc] initWithScrollView:table position:AllAroundPullViewPositionBottom action:^(AllAroundPullView *view){ [self loadCharacters]; }]; bottomPullView.hidden = YES; [table addSubview:bottomPullView];
loadCharacters
.I restkit.network:RKObjectRequestOperation.m:220 GET 'http://your-url.here' (200 OK / 20 objects)
, then everything is fine and you can check Whether our objects are preserved in the database.RKMarvel.sqlite
(this is the name we specified as a parameter when calling the addSQLitePersistentStoreAtPath:
method addSQLitePersistentStoreAtPath:
earlier). Let's open this database in SQLite-editor and make sure that our characters are saved:UITableView
(the author assumes that this is already known to the reader), but go straight to the table delegate method that creates the cells: - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { NSInteger row = indexPath.row; NSString *reusableIdentifier = [NSString stringWithFormat:@"%d", row % 2]; UITableViewCell *cell = [table dequeueReusableCellWithIdentifier:reusableIdentifier]; if (!cell) { cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:reusableIdentifier]; cell.autoresizingMask = UIViewAutoresizingFlexibleWidth; } [[cell.contentView subviews] makeObjectsPerformSelector:@selector(removeFromSuperview)]; if (numberOfCharacters > row) { Character *curCharacter = [Character charWithManagedObjectContext: [[GDMarvelRKObjectManager manager] managedObjectContext] andInnerID:row]; if (curCharacter) { BOOL charHasDescription = ![curCharacter.charDescription isEqualToString:@""]; UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(70, 0, CGRectGetWidth(cell.contentView.frame) - 70 - (charHasDescription ? 60 : 0), 60)]; label.backgroundColor = [UIColor clearColor]; label.text = curCharacter.name; label.autoresizingMask = UIViewAutoresizingFlexibleWidth; [cell.contentView addSubview:label]; GDCellThumbnailView *thumbnail = [GDCellThumbnailView thumbnail]; if (curCharacter.thumbnailImageData) [thumbnail setImage:[UIImage imageWithData:curCharacter.thumbnailImageData]]; else [self loadThumbnail:thumbnail fromURLString:curCharacter.thumbnailURLString forCharacter:curCharacter]; [cell.contentView addSubview:thumbnail]; cell.accessoryType = charHasDescription ? UITableViewCellAccessoryDetailButton : UITableViewCellSelectionStyleNone; cell.selectionStyle = charHasDescription ? UITableViewCellSelectionStyleGray : UITableViewCellSelectionStyleNone; } } return cell; }
GDCellThumbnailView
, instances of which I put on the cell. He does not do anything special, he just has the opportunity to show us the “spinning flower” of waiting until the thumbnail is loaded.loadThumbnail:fromURLString:forCharacter:
our main view controller will now look like this:AFNetworking
framework, we will use it to send an asynchronous request to the Marvel servers to download images: - (void)loadThumbnail:(GDCellThumbnailView *)view fromURLString:(NSString *)urlString forCharacter:(Character *)character { XLog(@"Loading thumbnail for %@", character.name); AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:urlString]]]; [operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) { character.thumbnailImageData = responseObject; [self saveToStore]; [view setImage:[UIImage imageWithData:responseObject]]; } failure:^(AFHTTPRequestOperation *operation, NSError *error) { XLog(@"%@", [error localizedDescription]); }]; [operation start]; }
Source: https://habr.com/ru/post/220885/
All Articles