There is a categroy UIAlertView+AFNetworking.h ship with AFNetworking 2.0. And I use it to show error messages in my App like this:
[UIAlertView showAlertViewForTaskWithErrorOnCompletion:task delegate:nil]
But I want to add some other messages in the message of UIAlertView.
i.e.:
StatusCode: 422
JSON Data: {"errors":{"detail":"Wrong password"}}
How to get "Wrong password" and show in a UIAlertView by the functions in UIAlertView+AFNetworking?
UIAlertView+AFNetworking category uses error messages from NSError – localizedDescription, localizedRecoverySuggestion and localizedFailureReason. AFNetworking generates NSError on failure, however, you must write your own responseSerializer to set your custom error messages there.
This topic has been thoroughly discussed on GitHub and I use solution based #camdez's code snippet.
My error response messages from API looks like this:
{ error: { message: "This username already exists" } }
Now, I've subclassed AFJSONResponseSerializer and when an error occurs, I save the error message to the NSError instance.
CustomResponseSerializer.h
#interface CustomResponseSerializer : AFJSONResponseSerializer
#end
CustomResponseSerializer.m
#implementation CustomResponseSerializer
- (id)responseObjectForResponse:(NSURLResponse *)response
data:(NSData *)data
error:(NSError *__autoreleasing *)error
{
id JSONObject = [super responseObjectForResponse:response data:data error:error]; // may mutate `error`
if (*error && [JSONObject objectForKey:#"error"]) {
NSMutableDictionary *mutableUserInfo = [(*error).userInfo mutableCopy];
mutableUserInfo[NSLocalizedFailureReasonErrorKey] = [[JSONObject objectForKey:#"error"] objectForKey:#"message"];
NSError *newError = [NSError errorWithDomain:(*error).domain code:(*error).code userInfo:[mutableUserInfo copy]];
(*error) = newError;
}
return JSONObject;
}
#end
Finally, you need to set appropriate responseSerializer on AFHTTPSessionManager (or AFHTTPRequestOperationManager if you use NSURLConnection API) instance. If you subclass this class as I do, you can simply do it in your init method by following:
self.responseSerializer = [CustomResponseSerializer serializer];
Related
I'm learning to develop iOS applications and now I'm reading some Objective-C source code.
This is a method to get user profile.
+ (void)getProfile:(void (^)(NSString *message))completion {
NSDictionary *dic = #{#"module":#"profile"};
[[self defaultManager] POST:KBaseUrl parameters:dic success:^(AFHTTPRequestOperation *operation, id responseObject) {
if ([self jsonOKForResponseObject:responseObject] && [self checkLogin:responseObject]) {
[ProfileManager sharedInstance].rank = responseObject[#"Variables"][#"space"][#"group"][#"grouptitle"];
[ProfileManager sharedInstance].credit = responseObject[#"Variables"][#"space"][#"credits"];
[ProfileManager sharedInstance].gender = responseObject[#"Variables"][#"space"][#"gender"];
completion(nil);
} else {
completion(#"fail");
}
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
completion(#"fail");
}];
}
My question is about the completion block.
I suppose that the completion block returns void and receives an NSString parameter.
In the block, what does completion(nil) mean?
Does that mean the block completion calls it self and send nil as parameter?
Doesn't that conflict with the parameter's type NSString*?
I'm not quite familiar with block in ObjC. Can anyone give a hint?
Yes you are right. It calls itself and sends nil as a parameter and it doesn't conflict with the NSString parameter. You are just passing nil to the NSString param.
You can call the above method like:
[YourClass getProfile:^(NSString *message) {
//message will be nill if you pass completion(nil);
}];
So when you pass the nill in the completion block, the message in the above method call will be nil!
The completion block is to notify you that your method call is complete, and at this point you can let that method pass certain paramteres , and if we consider your method:
+ (void)getProfile:(void (^)(NSString *message))completion {
NSDictionary *dic = #{#"module":#"profile"};
[[self defaultManager] POST:KBaseUrl parameters:dic success:^(AFHTTPRequestOperation *operation, id responseObject) {
if ([self jsonOKForResponseObject:responseObject] && [self checkLogin:responseObject]) {
.....
completion(#"hey sucess");
} else {
completion(#"if loop failed");
}
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
completion(#"some error occured");
}];
}
and when you call the method getProfile:
[YourClass getProfile:^(NSString *message) {
//Execution will reach here when the method call for getPRofile is complete and you have a result which you just sent through the completion block as a parameter which is a string and is waiting for you to process.
//you can do more from here
if([message isEqualToString:#"hey success"]){
//do something
}
if([message isEqualToString:#"if loop failed"]){
//do something
}
if([message isEqualToString:#"some error occured"]){
//do something
}
}];
As per #rmaddy comment, iis always a good practice to use BOOL to indicate the status success or fail rather than depending on a string as string can get localized/changed. We shold use the string to get more description of the error.
So your block should be:
+ (void)getProfile:(void (^)(BOOL status,NSString *message))completion {
.....
completion(YES,#"hey success");
}
and you can call it like":
[YourClass getProfile:^(BOOL status, NSString *message) {
if(status){
//get the success message
}else{
//get the fail message
}
}];
Blocks had a lot of type like :-
As a local variable:
returnType (^blockName)(parameterTypes) = ^returnType(parameters) {...};
As a property:
#property (nonatomic, copy, nullability) returnType (^blockName)(parameterTypes);
As a method parameter:
- (void)someMethodThatTakesABlock:(returnType (^nullability)(parameterTypes))blockName;
As an argument to a method call:
[someObject someMethodThatTakesABlock:^returnType (parameters) {...}];
As a typedef:
typedef returnType (^TypeName)(parameterTypes);
TypeName blockName = ^returnType(parameters) {...};
read more about it from here
Completion block does not return anything. It is just a piece of code to be executed, thats all. Though, you can give it some input where you call it so you can use the result elsewhere.
NSString *message is the input for your block so when you call your function getProfile as:
[MyClass getProfile:^(NSString *message) {
// write code to be executed when getProfile function finishes its job and sends message here.
}];
[MyClass getProfile:nil];
When used like this you're preferring not to do anything when getProfile finishes its job.
You are probably mixing your network manager's function's completion block with the one you wrote.
I am facing app extension close issues , please tell me if anyone know what wrong am doing.I am using action extension after preform some action inside extension i need to return response back.
Sample Code
// With Success Case
- (void) completeActionWithItems: (NSString *) response {
NSExtensionItem *extensionItem = [[NSExtensionItem alloc] init];
extensionItem.attachments = #[[[NSItemProvider alloc] response typeIdentifier: (NSString *)kUTTypePlainText]];
[self.extensionContext completeRequestReturningItems: #[extensionItem] completionHandler: nil];
}
// With Error Case
- (void) completeActionWithError: (NSError *) error {
[self.extensionContext cancelRequestWithError: error];
}
With Success Case working fine but some time is not closing,
With Error Case not working above code.
Please let me know what went wrong.Thanks
When you create an action extension, this is the default method which will close the Action Extension View Controller:
- (IBAction)done {
// Return any edited content to the host app.
// This template doesn't do anything, so we just echo the passed in items.
[self.extensionContext completeRequestReturningItems:self.extensionContext.inputItems completionHandler:nil];
}
Since this method is already provided, you should just try calling it from your success method.
// With Success Case
- (void) completeActionWithItems: (NSString *) response {
NSExtensionItem *extensionItem = [[NSExtensionItem alloc] init];
extensionItem.attachments = #[[[NSItemProvider alloc] response typeIdentifier: (NSString *)kUTTypePlainText]];
[self.extensionContext completeRequestReturningItems: #[extensionItem] completionHandler: nil];
// Call to "done" method
[self done];
}
I have a problem with SoundCloud API in my application.
I have created an app, what get link to the track (e.g. something like this http://api.soundcloud.com/tracks/48760960) and get stream_url, what later can be played using SCAudioStream. There is no auth for users, I use only client id and client secret of my app. App is registered.
I create soundCloudManager this way:
- (id)init
{
if(self = [super init])
{
conf = [SCSoundCloudAPIConfiguration configurationForProductionWithClientID: kSCClientID
clientSecret: kSCClientSecret
redirectURL: kSCRedirectURL];
conf.accessTokenURL = kSCAccessTokenURL;
api = [[SCSoundCloudAPI alloc] initWithDelegate: self
authenticationDelegate: self
apiConfiguration: conf];
//[api checkAuthentication];
}
return self;
}
If uncomment [api checkAuthentication], then logs will says next:
"auth ready with URL:https://soundcloud.com/connect?client_id=&redirect_uri=&response_type=code"
Then I call this method to get data from track:
- (void)runSearchingStreamWithTrackID: (NSString *)trackID
{
[api performMethod: #"GET"
onResource: #"tracks"
withParameters: [NSDictionary dictionaryWithObject:trackID forKey:#"ids"]
context: #"trackid"
userInfo: nil];
}
And then this delegate's method calls:
- (void)soundCloudAPI:(SCSoundCloudAPI *)soundCloudAPI
didFailWithError:(NSError *)error
context:(id)context
userInfo:(id)userInfo
Text of error:
"HTTP Error: 401".
It means unauthorized. But why? Should I be authorized as soundCloud user to getting info about tracks?
I'm using this for SoundCloudAPI:
https://github.com/soundcloud/cocoa-api-wrapper
Please, help! :(
Today I fixed it. I just add this method to init :
[api authenticateWithUsername:kLogin password:kPwd];
After it this method was called:
- (void)soundCloudAPIDidAuthenticate
So, this test account was authorized.
Then I call this method:
- (void)runSearchingStreamWithTrackID: (NSString *)trackID
{
[api performMethod: #"GET"
onResource: #"tracks"
withParameters: [NSDictionary dictionaryWithObject:trackID forKey:#"ids"]
context: #"trackid"
userInfo: nil];
}
And no one of these methods will be called:
- (void)soundCloudAPI:(SCSoundCloudAPI *)soundCloudAPI
didFailWithError:(NSError *)error
context:(id)context
userInfo:(id)userInfo;
- (void)soundCloudAPI:(SCSoundCloudAPI *)soundCloudAPI
didFinishWithData:(NSData *)data
context:(id)context
userInfo:(id)userInfo;
- (void)soundCloudAPIDidAuthenticate;
- (void)soundCloudAPIDidResetAuthentication;
- (void)soundCloudAPIDidFailToGetAccessTokenWithError:(NSError *)error;
- (void)soundCloudAPIPreparedAuthorizationURL:(NSURL *)authorizationURL;
But there is log:
-[NXOAuth2PostBodyStream open] Stream has been reopened after close
And this method was called:
[NXOAuth2Client oauthConnection:connection didFailWithError:error];
error: HTTP 401
What do I wrong?
I understood the problem. Just download the SC API from the official site and install the latest SC API. Then do something like this:
- (void)searchStreamURLByTrackID: (NSString *)trackID
{
NSString *string = [NSString stringWithFormat:#"https://api.soundcloud.com/tracks/%#.json?client_id=%#", trackID, kSCClientID];
[SCRequest performMethod: SCRequestMethodGET
onResource: [NSURL URLWithString:string]
usingParameters: nil
withAccount: nil
sendingProgressHandler: nil
responseHandler:^(NSURLResponse *response, NSData *data, NSError *error){
NSString *resultString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(#"FOUND:\n%#", resultString);
}
];
}
After that obtain the stream url from the JSON in the resultString.
Finally, you have to write this code:
NSString *streamURL = [(NSDictionary *)jsonData objectForKey:#"stream_url"];
NSString *realURL = [streamURL stringByAppendingFormat:#".json?client_id=%#", kSCClientID];
AVPlayer *player = [[AVPlayer alloc] initWithURL:[NSURL URLWithString:realURL]];
Enjoy!
Did you check https://github.com/soundcloud/CocoaSoundCloudAPI ?
This is the more current version of the API wrapper (if you don't need iOS 3.0 support).
It also comes with a tutorial video: http://vimeo.com/28715664
If you want to stay with this library you should try to figure out where the log message ("auth ready with ...") appears. Did you implement any of the AuthorizationDelegate methods?
See https://github.com/soundcloud/cocoa-api-wrapper/blob/master/Usage.md#the-authentication-delegate-the-easy-way
I am using RESTKIT to map the JSON returned from server.
The JSON result obtained from server is as follows
{"term":"Zh","results":{"users":[{"id":{"$oid":"4ebe59970a614e0019000055"},"term":"some text","score":1}]}
How can I convert the above JSON result to the below:
{"results":{"users":[{"uid":"4ebe59970a614e0019000055","text":"some text"}]}
Also, where can I do this so that the RESTKIT mapping will use the converted JSON instead of the initial one?
Below is the loader class that I am using to manage the JSON and mappings
-(void)getObjects
{
RKObjectManager *sharedManager = [RKObjectManager sharedManager];
[sharedManager loadObjectsAtResourcePath:self.resourcePath delegate:self];
}
- (void)request:(RKRequest*)request didLoadResponse:(RKResponse*)response {
NSLog(#"Loaded PAYLOAD successfully for %#, with response %#", self.resourcePath , [response bodyAsString] );
}
- (void)objectLoader:(RKObjectLoader*)objectLoader didLoadObjects:(NSArray*)objects
{
}
+ (void)setManagerAndMappings
{
RKObjectManager* manager = [RKObjectManager objectManagerWithBaseURL:SERVER_URL];
RKObjectMappingProvider* provider = [[RKObjectMappingProvider new] autorelease];
//User Object Mapping
RKObjectMapping* userMapping = [RKObjectMapping mappingForClass:[User class]];
[userMapping mapKeyPath:#"_id" toAttribute:#"uid"];
[userMapping mapAttributes:#"avatar_url",#"created_at",#"email",#"name",#"nickname",#"follower_count",#"following_count",#"answer_count",#"new_notification_count",#"new_questions_count",#"is_following",#"facebook_configured",#"twitter_configured",#"description",#"approved",#"type", nil];
[provider setMapping:userMapping forKeyPath:#"user"];
}
#Shocks answer is appealing, unfortunately it is valid a for version before 0.20.
Anyway, a similar solution is available for 0.20 too, using an implementation of RKSerialization:
#interface ORRKJsonSerialization : NSObject <RKSerialization>
#end
and implementing
#implementation ORRKJsonSerialization
+ (id)objectFromData:(NSData *)data error:(NSError **)error
{
id result = [NSJSONSerialization JSONObjectWithData:data options:0 error:error];
// change your data before mapping
return result;
}
+ (NSData *)dataFromObject:(id)object error:(NSError **)error
{
return [NSJSONSerialization dataWithJSONObject:object options:0 error:error];
}
#end
then during the setup:
[RKMIMETypeSerialization registerClass:[ORRKJsonSerialization class] forMIMEType:#"application/json"];
HTH
There is a willMapData: selector in the RKObkectLoaderDelegate that is invoked just after parsing has completed. The mappableData argumet is mutable, so i guess you can change the data just before the object mapping will take place.
use RKObjectRequestOperation.setWillMapDeserializedResponseBlock:.
In swift:
let request = RKObjectManager.sharedManager().requestWith...
let operation = RKObjectManager.sharedManager().managedObjectRequestOperationWithRequest(request, managedObjectContext: context, success: { operation, result in
// success
}, failure: { operation, error in
// failure
})
operation.setWillMapDeserializedResponseBlock { deserializedResponse -> AnyObject! in
// Here to transform response
return transformResponse(deserializedResponse)
}
RKObjectManager.sharedManager().enqueueObjectRequestOperation(operation)
For me the only solution is to modify the returned object at the server level.
If you can't,just map that returns the server.
You can register your own parser. I had to do this to modify and clean-up some JSON from a legacy system.
[[RKParserRegistry sharedRegistry] setParserClass:[MyJSONParser class]
forMIMEType:#"application/json"];
And then create your own:
#import <RestKit/RestKit.h>
#import <RestKit/RKJSONParserJSONKit.h>
#import <RestKit/JSONKit.h>
#import <RestKit/RKLog.h>
#interface EFJSONParser : RKJSONParserJSONKit
- (NSDictionary *)objectFromString:(NSString *)string error:(NSError **)error;
#end
It's very difficult to editing the response of JSOn. the better way is only to do changes in server side as said by #beber
I'd like to use RestKit and handle several different requests in the same class, i.e. in the didLoadResponse: method. How can I distinguish between the different requests? How do I know which request is finished?
I'm doing the request via
RKClient *client = [RKClient sharedClient];
[client get:#"/....", method] delegate:self];
Then, in the delegate-method
- (void)request:(RKRequest *)request didLoadResponse:(RKResponse *)response {
if (???) // request which gets XY returned
...
else if (???) // request which gets YZ returned
...
}
is that possible?
Sure, the RKClient get: method returns a RKRequest object. Just set a userData to the request and retrieve it later in the delegate.
RKClient *client = [RKClient sharedClient];
RKRequest *request = [client get:#"/....", method] delegate:self];
[request setUserData:#"FirstRequest"];
and check it later in the delegate
- (void)request:(RKRequest *)request didLoadResponse:(RKResponse *)response {
id userData = [request userData];
if ([userData isEqual:#"FirstRequest"]) // request which gets XY returned
...
else if (...) // request which gets YZ returned
...
}
This isn't an exact answer to your question, but I have the feeling that some people will come here wondering how to distinguish multiple requests in didLoadObjects, as I did. The solution is to use isKindOfClass.
For example, I make two HTTP calls when a user logs into my app, and I want to distinguish the object returned from the getUser call from the object returned by getSummary (because if I don't then it crashes). This code checks if the returned object is a "kind of" that particular class, and if so sets the object to a local instance of that object.
- (void)objectLoader:(RKObjectLoader*)objectLoader didLoadObjects:(NSArray*)objects {
if ([[objects objectAtIndex:0] isKindOfClass:[APIUser class]]) {
APIUser *apiUser = [objects objectAtIndex:0];
}
else if ([[objects objectAtIndex:0] isKindOfClass:[APIUserSummary class]]) {
APIUserSummary *summary = [objects objectAtIndex:0];
}
}