I need help pulling in two APIs with RestKit.
I have API1 modeled, and pulling in correctly already.
The problem is trying to figure out how to pull API2 in to the ViewController.
Specifically, I already have the model class set up, but in the ViewController where the results of API1 + API2 will display, I can't figure out how to work it into my viewDidLoad.
Thanks!
AppDelegate.m
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
// RestKit
NSString *baseURL = #"http://api.firstwebsite.com/v1";
RKObjectManager *manager = [RKObjectManager sharedManager];
if (!manager) {
manager = [RKObjectManager objectManagerWithBaseURLString:baseURL];
manager.client.serviceUnavailableAlertEnabled = YES;
manager.requestQueue.showsNetworkActivityIndicatorWhenBusy = YES;
} else {
manager.client.baseURL = [RKURL URLWithString:baseURL];
}
return YES;
}
WebListViewController.m
#property (strong, nonatomic) NSArray *hArray;
- (void)viewDidLoad
{
[super viewDidLoad];
[[RKObjectManager sharedManager] loadObjectsAtResourcePath:
[NSString stringWithFormat:
#"/something/?limit=100&something=%#&something=%#&apikey=xxxx", var1, var2]
usingBlock:^(RKObjectLoader *loader) {
loader.onDidLoadObjects = ^(NSArray *objects){
hArray = objects;
[_tableView reloadData];
};
[loader.mappingProvider setMapping:[Fe mapping] forKeyPath:#"fe"];
loader.onDidLoadResponse = ^(RKResponse *response){
//NSLog(#"BodyAsString: %#", [response bodyAsString]);
};
}];
}
Abstract your view controller (and app delegate) away from knowledge of where the data is coming from - they have no business knowing. The app delegate shouldn't really know anything about any of this. The view controller should know that data exists and that additional data can be requested, but this should be in terms of the internal app data model, not the external model or the source of the data.
So, create a data controller. Usually a singleton. Provide an interface to get / set / request / update the data model based on set criteria. Treat all calls as asynchronous with completion callback blocks.
Internally, this data controller can manage multiple object managers, each with a different base URL , mappings and descriptors, but this is all internal knowledge.
Related
I'm a junior level developer and I'm stuck at this very simple thing which I'm unable to figure out.
I've a class which is AFHTTPRequestOperationManager extended. Means it's #interface apiClient : AFHTTPRequestOperationManager in this class I've all my code which fetch images from Imgur API and using AFNetworking I've parsed the data upto a level where I start getting only the link of the images.Now the rootViewController which is a UICollectionViewController extended. In it's viewDidLoad I send a call to my APIClient and it start moving from methods to methods and finally give me an NSMutableArray which contains the images link I'll use in the UIImageView of my CollectionViewCell.
The Question is I'm using this to send the final links back to my rootViewController i.e. GalleryCollectionViewController
GalleryCollectionViewController *gcvc = [[GalleryCollectionViewController alloc] init];
[gcvc recieveGalleryImagesLinks:galleryImgLinkArr];
The problem is that in gcvc it calls numberOfItemsInSection before it can get any response from the API. So that means the count goes out zero and hence it is not displaying the data. So how can I get it to get the API call from apiClient class first and then make the viewController. I hope I've clearly stated my problem and if there is a need of sharing any more code I'll do it.
UPDATE
After authorization this methods gets the gallery images:
- (void)galleryAlbum:(NSString *)ID {
NSLog(#"ID is %#", ID);
[self GET:[NSString stringWithFormat:#"%#3/gallery/album/%#", self.baseURL,ID] parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) {
self.galleryData = (NSDictionary *)responseObject;
galleryDataArr = [[NSMutableArray alloc] init];
galleryDataArr = [self.galleryData valueForKey:#"data"];
galleryImagesArr = [[NSMutableArray alloc] init];
galleryImagesArr = [galleryDataArr valueForKey:#"images"];
galleryImgLinkArr = [[NSMutableArray alloc] init];
galleryImgLinkArr = [galleryImagesArr valueForKey:#"link"];
GalleryCollectionViewController *gcvc = [[GalleryCollectionViewController alloc] init];
[gcvc recieveGalleryImagesLinks:galleryImgLinkArr];
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"Failed #2");
}];
}
Here gcvc is receiving the array with images link and then reloading the collection view as:
-(void)recieveGalleryImagesLinks:(NSMutableArray *)array
{
self.imageLinks = [[NSMutableArray alloc] initWithArray:array];
NSLog(#"Array: %#",self.imageLinks);
[self.collectionView reloadData];
}
But it's giving "UICollectionView: must be initialized with a non-nil layout parameter" although the array is not empty.
This line of code below is incorrect:
GalleryCollectionViewController *gcvc = [[GalleryCollectionViewController alloc] init];
[gcvc recieveGalleryImagesLinks:galleryImgLinkArr];
You need to use a blocks or delegate approach here which will pass back the array back to your collectionViewController and reload the collecrionView.
Even your method call from API class seems to be correct for delegate approach but make it a protocal method. This way you need not have to change much. It will be something like below:
[self.delegate recieveGalleryImagesLinks:galleryImgLinkArr
And when are calling API and is waiting for response show activityIndicator for loading data.
I'm looking for a way to handle "generic" errors such as request timeouts or for when the connection goes offline.
Basically, I have multiple (singleton) subclasses of AFHTTPSessionManager where each one represents a client that handles requests to different servers. Each client is setup by overriding initWithBaseURL as recommended by the author of AFNetworking; this is where the request/response serializers as well as generic headers are set. Here's a sample client:
#implementation APIClient
+ (APIClient *)sharedClient {
static APIClient *sharedClient = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedClient = [[self alloc] initWithBaseURL:[NSURL URLWithString:#"baseurl.goes.here"]];
});
return sharedClient;
}
- (instancetype)initWithBaseURL:(NSURL *)url
{
self = [super initWithBaseURL:url];
if(self) {
// Setup goes here
self.requestSerializer = [AFHTTPRequestSerializer serializer];
self.requestSerializer.timeoutInterval = 20.0f;
self.responseSerializer.acceptableContentTypes = [NSSet setWithObjects:#"text/plain", #"text/html", nil];
[AFNetworkActivityIndicatorManager sharedManager].enabled = YES;
[AFNetworkActivityLogger sharedLogger].level = AFLoggerLevelDebug;
[[AFNetworkActivityLogger sharedLogger] startLogging];
}
return self;
}
- (void)startPostRequestWithPath:(NSString *)path parameters:(NSDictionary *)parameters successBlock:(APISuccessBlock)success failureBlock:(APIFailureBlock)failure
{
[self POST:path parameters:parameters
success:^(NSURLSessionDataTask * _Nonnull task, id _Nonnull responseObject) {
success(responseObject);
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
if(isGenericError) {
// Do something generic here
}
else {
failure(error);
}
}];
}
Inside my model (e.g, Post), I have a static method that can be used by the view controller to fetch the data by passing its own success/failure blocks to the client. So it goes like this:
View Controller --> Model --> Client --> Model --> View Controller.
And here's the implementation of the model
#implementation Post
+ (void)fetchLatestPost:(void (^)(Post *parsedData, NSError *error))completion
{
[[APIClient sharedClient] startRequestWithPath:kIndexPath
parameters:nil
requestType:RequestTypePost
successBlock:^(id data) {
NSError *parsingError = nil;
Post *post = [[Index alloc] initWithDictionary:data error:&err];
completion(index, nil);
}
failureBlock:^(NSError *error) {
completion(nil, error);
}
];
}
When a view controller tries to fetch that Post and the request times out, I'd like to hide the contents of the screen and show a refresh button; this logic is implemented in my BaseViewController so that all view controllers can reuse it. The question is, how do I restart the SAME request when that button is clicked? Do note that a view controller can make multiple requests from different models with different method signatures. Any help would be greatly appreciated as I can't seem to figure this out at all.
I used to handle this using delegates, where the BaseViewController would implement the "generic" delegate methods. However, I've been trying to switch to blocks and while it does have its advantages, it doesn't allow me to make use of my BaseViewController since it's can't "override" the view controller's failure blocks.
Started down the path of 'singleton'. (whew!!) Now, trying to retrace some steps AND take the core data stack OUT of the app delegate.
In a discussion of this, I saw the following code:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions(NSDictionary *)launchOptions {
DataModel *dataModel = [[DataModel alloc] init];
self.rootViewController.dataModel = dataModel;
...
}
In DataModel.m is the core data stack, methods to initiate a web service and methods to save the returned data into core data. Connection is another class.
Launch, starting the connection, passing the managed object context to the root view controller and receiving the web data all work fine. Then, a posted notification (upon completion of data receipt) in Connection class notifies DataModel of data to process. The issue is DataModel has been deallocated. The app crashes.
After abandoning the (shhh) singleton class for DataModel, the question(s) then are: How can the DataModel be kept around to process the received web data, destined for core data? Or, would it be better to split the core data stack and processing the received data into 2 files?
It seems logical to have the core data stack and processing methods in one class. I want to build this app by passing the context from controller to controller.
I have a class, "SVODataHelper" that has this in it:
#pragma mark - Setup
- (id) init
{
self = [super init];
if (!self)
return nil;
_model = [NSManagedObjectModel mergedModelFromBundles:nil];
_coordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:_model];
_context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
[_context setPersistentStoreCoordinator:_coordinator];
return self;
}
- (void) loadStore
{
if (_store)
return;
NSError *error = nil;
_store = [_coordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:[self storeURL] options:nil error:&error];
if (!_store)
{
NSLog(#"Failed to add store. Error: %#", error);
abort();
}
}
- (void) setupCoreData
{
[self loadStore];
}
Then, in AppDelegate, I use:
- (SVOData *)svoData
{
if (!_svoDataHelper)
{
static dispatch_once_t predicate;
dispatch_once (&predicate, ^{ _svoDataHelper = [[SVOData alloc] init]; }); // insures a single instance created to guarantee thread safety.
[_svoDataHelper setupCoreData];
}
return _svoDataHelper;
}
And finally, I access the helper class with:
svoDataHelper = [(AppDelegate *)[[UIApplication sharedApplication] delegate] svoData];
I don't know if it is good or bad but it works. I got the concept from some book I read ("Learning IOS Core Data?) and it looked pretty clean as it allowed me to keep the essential stuff in a separate class while avoiding having to pass a pointer to that class around.
I have a lot of classes which are sending requests and finally it all comes to SplitViewController. In the SplitUIviewclass I have to long poll and write the data in a table view. The long polling is done in the background thread, so I have declared a variable in app delegate, but when it comes to that it is nil. And the problem is whenever I try to access the NSMutablearray through the appdelegate, its coming as nil and the array is being released. My code for long polling is
- (void) longPoll {
#autoreleasePool
{
//compose the request
NSError* error = nil;
NSURLResponse* response = nil;
NSURL* requestUrl = [NSURL URLWithString:#"http://www.example.com/pollUrl"];
NSURLRequest* request = [NSURLRequest requestWithURL:requestUrl];
//send the request (will block until a response comes back)
NSData* responseData = [NSURLConnection sendSynchronousRequest:request
returningResponse:&response error:&error];
//pass the response on to the handler (can also check for errors here, if you want)
[self performSelectorOnMainThread:#selector(dataReceived:)
withObject:responseData waitUntilDone:YES];
}
//send the next poll request
[self performSelectorInBackground:#selector(longPoll) withObject: nil];
}
- (void) startPoll {
[self performSelectorInBackground:#selector(longPoll) withObject: nil];
}
- (void) dataReceived: (NSData*) theData {
//i write data received here to app delegate table
}
If I call any other method in my SplitView class from data received, I'm losing control, also I cannot print my app delegate values in data received or the variables being released, I cannot call reload table or any other method from here.
Cant you set your properties in your ViewControllers as strong/retain like so
property (strong,retain) NSMutableArray *myData;
BTW, I learned a moment ago that it is bad practise to use your AppDelegate as a storage place for global containers. The ApplicationDelegate is a place for application delegate methods and for the initial setup of the foundation of your app; such as setting up the navigationController.
So consider storing your data in the appropriate place, perhaps core data or something else.
I am seeing a really weird and random issue in my code that I can't track down. I am getting crashes in my data model init methods when returning from AFNetworking JSON request methods. When the app does crash I am able to step back in the call stack to debug the what the JSON request/response was. The weird part is when I check the URL, request, and resonseJSON. The responseJSON does not match the expected result of the URL/request. It's like I am getting some other API methods call and data. Because the data/JSON is not what I expect the app will crash on model init.
The data I get back is usually different and not always the same. Sometimes the data is from endpoint A and sometimes it is from B, it's never consistent. It does however seem to crash consistently in the same model object.
Request endpoint A data but I get back endpoint B data. When I debug the AFHttpOperation when it crashes I see this is the result. It's almost like 2 calls are getting crossed and is some type of race condition. Below is a sample of my model object, Rest client, and model access layer.
Model Object
#implementation Applications
- (id)initWithData:(NSDictionary *)appData forLocation:(Location *)location inCategory:(Category *)category {
// appData is the JSON returned by The Rest Client and AFNetworking
self = [super init];
DDLogVerbose(#"appData = %#", appData);
if (self) {
_location = location;
_listeners = [NSMutableArray mutableArrayUsingWeakReferences];
_devices = [[NSMutableDictionary alloc] init];
_category = category;
_subscriptions = [Utility sanitizeArray:appData[#"Subscriptions"]];
}
return self;
}
#end
#implementation Location
- (void)refreshApplications {
[[Model shared] appsForLocation:self
success:^(NSObject *obj) {
self.apps = nil; //we have to get our apps again
self.apps = [NSMutableArray array];
NSArray *newApps = (NSArray *) obj;
for (NSDictionary *app in newApps) {
**// This is where it's crashing!**
Applications *newApp = [[Applications alloc] initWithData:app
forLocation:self
inCategory:[[SmartAppCategory alloc] init]];
[self.apps addObject:newApp];
}
[self notifyListeners];
}
error:nil];
}
#end
Rest Client
#interface Rest
+ (Rest *)sharedClient;
- (void)GET:(NSString *)path parameters:(NSDictionary *)params success:(SuccessCallback)sCallback error:(ErrorCallback)eCallback;
#end
#implementation Rest
+ (Rest *)sharedClient {
static dispatch_once_t token;
static Rest *shared = nil;
dispatch_once(&token, ^{
shared = [[Rest alloc] init];
});
return shared;
}
- (id)init {
self = [super init];
if (self) {
[self createClients];
}
return self;
}
- (void)createClients {
// Setup the Secure Client
// Private implementation properties
self.secureClient = [[AFOAuth2Client alloc] initWithBaseURL:baseUrl clientID:OAUTH2_CLIENT_ID secret:OAUTH2_CLIENT_SECRET];
[self.secureClient setParameterEncoding:AFJSONParameterEncoding];
AFOAuthCredential *credential = (AFOAuthCredential *) [NSKeyedUnarchiver unarchiveObjectWithData:[KeyChainStore dataForKey:KEYCHAIN_SETTINGS_AFOAuthCredential]];
if (credential) {
[self.secureClient setAuthorizationHeaderWithToken:credential.accessToken];
}
// Setup the Anonymous Client
self.anonymousClient = [[AFHTTPClient alloc] initWithBaseURL:baseUrl];
[self.anonymousClient setParameterEncoding:AFJSONParameterEncoding];
[self.anonymousClient registerHTTPOperationClass:[AFJSONRequestOperation class]];
}
- (void)GET:(NSString *)path parameters:(NSDictionary *)params success:(SuccessCallback)sCallback error:(ErrorCallback)eCallback {
[_secureClient getPath:path
parameters:params
success:^(AFHTTPRequestOperation *operation, id responseObject) {
DDLogVerbose(#"Success Path: %# JSON: %#", path, responseObject);
if (sCallback) sCallback(responseObject);
}
failure:^(AFHTTPRequestOperation *operation, NSError *error) {
[Rest callErrorBlock:eCallback withOperation:operation];
}];
}
#end
Model Access Layer
#interface Model
+ (Model *)shared;
- (void)appsForLocation:(Location *)location success:(SuccessCallback)success error:(ErrorCallback)error;
#end
#implementation Model
- (void)appsForLocation:(Location *)location success:(SuccessCallback)success error:(ErrorCallback)error {
NSString *path = [NSString stringWithFormat:#"/api/locations/%#/apps/", location.locationId];
[[Rest sharedClient] GET:path parameters:nil success:success error:error];
}
#end
A Location is a root object in the application and it will be told to refresh often. Either through UI interaction, events, or data Deserialization the the refreshApplications will execute to get more data from the server. Meanwhile other requests and events are going on in the application to get and send data to the API is JSON. Some of these GET calls to other endpoints seem to be messing with the response data.
Questions
How could this be happening with AFNetworking?
Am I being too quick to blame AFNetowrking and should I be looking for other places in my system that could be crossing the responses? I do have a load balanced backend hosted at Amazon.
Is this an endpoint issue?
How can I better debug and reproduce this issue? It only comes up at random times and is very hard to replicate. I have to continually run and re-run the application in hopes that it is crash.
Are there any advanced debugging techniques that I can use to back trace this call/crash using xcode?
I recommend that you use Charles proxy to double-check that the data you're receiving is correct. There's a trial version available that works identically to the registered version for 30 days. My first guess is that there's either some sort of buggy cache layer between you and your server, or your server is buggy. An HTTP proxy like Charles will allow you to confirm or reject this hypothesis.
This page explains how to set up Charles to proxy non-HTTPS connections from iOS devices.
To debug non-HTTPS as well as HTTPS Traffic use the mitmproxy
It allows you to inspect all packages and also resend them and much more.
With this you can check what really happens and if the backend is the problem or if AFNetworking has a Bug.
And as a cool side effect mitmproxy is totally free and Open-Sourced under the MIT Licensed.
On their website you will find some handy tutorials specific for iOS.