Objective-C Generic Response Types? - objective-c

I have a wrapper for an API written in Objective-C that looks roughly like so:
#interface MyAPI : NSObject
- (void)getUsers :(UserRequest*)request :(void (^)(UserResponse *response))onResponse :(void (^)(APIError *error))onError;
- (void)getPosts :(PostRequest*)request :(void (^)(PostResponse *response))onResponse :(void (^)(APIError *error))onError;
- (void)getOtherStuff :(OtherStuffRequest*)request :(void (^)(OtherStuffResponse *response))onResponse :(void (^)(APIError *error))onError;
#end
#implementation MyAPI
- (void)getUsers :(UserRequest*)request :(void (^)(UserResponse *response))onResponse :(void (^)(APIError *error))onError
{
>>> NSString *url = "/api/users";
HTTPClient *client = [[HTTPClient alloc] init];
HTTPRequest *httpRequest = [[HTTPRequest alloc] initWithURL:url];
httpRequest.queryParams = request.toDictionary;
[httpClient send:httpRequest success:^(HTTPResponse *httpResponse) {
NSError *error;
>>> UserResponse *response = [[UserResponse alloc] initWithString:httpResposne.body error:&error];
if (error == nil) {
onResponse(response);
} else {
APIError *errorResponse = [[APIError alloc] initWithError:error];
onError(errorResponse);
}
}, error:^(NSError *error) {
APIError *errorResponse = [[APIError alloc] initWithError:error];
onError(errorResponse);
}];
}
- (void)getPosts :(PostRequest*)request :(void (^)(PostResponse *response))onResponse :(void (^)(APIError *error))onError
{
>>> NSString *url = "/api/posts";
HTTPClient *client = [[HTTPClient alloc] init];
HTTPRequest *httpRequest = [[HTTPRequest alloc] initWithURL:url];
httpRequest.queryParams = request.toDictionary;
[httpClient send:httpRequest success:^(HTTPResponse *httpResponse) {
NSError *error;
>>> PostResponse *response = [[PostResponse alloc] initWithString:httpResposne.body error:&error];
if (error == nil) {
onResponse(response);
} else {
APIError *errorResponse = [[APIError alloc] initWithError:error];
onError(errorResponse);
}
}, error:^(NSError *error) {
APIError *errorResponse = [[APIError alloc] initWithError:error];
onError(errorResponse);
}];
}
- (void)getOtherStuff :(OtherStuffRequest*)request :(void (^)(OtherStuffResponse *response))onResponse :(void (^)(APIError *error))onError
{
>>> NSString *url = "/api/otherStuff";
HTTPClient *client = [[HTTPClient alloc] init];
HTTPRequest *httpRequest = [[HTTPRequest alloc] initWithURL:url];
httpRequest.queryParams = request.toDictionary;
[httpClient send:httpRequest success:^(HTTPResponse *httpResponse) {
NSError *error;
>>> OtherStuffResponse *response = [[OtherStuffResponse alloc] initWithString:httpResposne.body error:&error];
if (error == nil) {
onResponse(response);
} else {
APIError *errorResponse = [[APIError alloc] initWithError:error];
onError(errorResponse);
}
}, error:^(NSError *error) {
APIError *errorResponse = [[APIError alloc] initWithError:error];
onError(errorResponse);
}];
}
#end
I've marked with >>> the only two lines that change from one method call to another - just the URL and the response type.
Ideally to avoid duplicate code I would like to do something like this:
#interface InternalImplementation<RequestType, ResponseType> : NSObject
+ (ResponseType)getAnything :(RequestType)request fromURL:(NSString*)url;
#end
#implementation InternalImplementation
+ (void)getAnything :(RequestType)request fromURL:(NSString*)url :(void (^)(ResponseType *response))onResponse :(void (^)(APIError *error))onError
{
HTTPClient *client = [[HTTPClient alloc] init];
HTTPRequest *httpRequest = [[HTTPRequest alloc] initWithURL:url];
httpRequest.queryParams = request.toDictionary;
[httpClient send:httpRequest success:^(HTTPResponse *httpResponse) {
NSError *error;
ResponseType *response = [[ResponseType alloc] initWithString:httpResposne.body error:&error];
if (error == nil) {
onResponse(response);
} else {
APIError *errorResponse = [[APIError alloc] initWithError:error];
onError(errorResponse);
}
}, error:^(NSError *error) {
APIError *errorResponse = [[APIError alloc] initWithError:error];
onError(errorResponse);
}];
}
#end
This way I could re-write my API with almost no code duplication:
#implementation MyAPI
- (void)getUsers :(UserRequest*)request :(void (^)(UserResponse *response))onResponse :(void (^)(APIError *error))onError
{
[InternalImplementation<UserRequest, UserResponse> getAnything:request fromURL:"/api/users" onResponse:onResponse onError:onError];
}
- (void)getPosts :(PostRequest*)request :(void (^)(PostResponse *response))onResponse :(void (^)(APIError *error))onError
{
[InternalImplementation<PostRequest, PostResponse> getAnything:request fromURL:"/api/users" onResponse:onResponse onError:onError];
}
- (void)getOtherStuff :(OtherStuffRequest*)request :(void (^)(OtherStuffResponse *response))onResponse :(void (^)(APIError *error))onError
{
[InternalImplementation<OtherStuffRequest, OtherStuffResponse> getAnything:request fromURL:"/api/users" onResponse:onResponse onError:onError];
}
#end
Unfortunately I can't quite figure out how to get this working. Can it even be done?

There's no need for generics here. Generics in Objective-C are "lightweight." They don't really do anything. They're mostly there to help bridge to Swift. (In theory they could provide better warnings about type mismatches in ObjC, but they rarely if ever do.)
Objective-C is duck typed. If something responds to a message, then it can accept that message. It doesn't matter what the "real" type of the object is. You can add a bunch of public methods like getUsers:... in order to get good warnings if the caller does something wrong, but getAnything can just accept Request. It doesn't need to be generic to do that (I'm assuming that UserRequest is a subclass of Request and UserResponse is a subclass of Response).
The only tricky thing is that you need to pass the class so you can init it. That's no problem:
+ (void)getAnythingWithRequest:(Request *)request
fromURL:(NSString *)url
ofType:(Class)responseType
success:(void (^)(Response *response))onResponse
failure:(void (^)(APIError *error))onError
With that, assuming that type has a initWithString:, you can just call it:
Response *response = [[responseType alloc] initWithString:#"..."];
Objective-C is a highly dynamic language. As long as you make sure everything responds to what you expect it to respond to, then it'll all work. There's no reason to add generics unless you intend to bridge this to Swift.
To make things a little nicer, getUsers:... etc can require that their success handler be of a more constrained type, and you can then just bridge across to that new type and add the URL string as you like:
+ (void)getUserWithRequest:(UserRequest *)request
ofType:(Class)responseType
success:(void (^)(UserResponse *response))onResponse
failure:(void (^)(APIError *error))onError {
[self getAnythingWithRequest:request
fromURL:#"/api/users"
ofType:[UserResponse self]
success:^(Response *response){onResponse((UserResponse*)response);}
failure:onError];
}

Related

ObjectiveC-When I add custom class to userInfo of NSUserNotification, it may cause userInfo to become nil value?

macOS 12.2.1
Xcode 13.3
I'm a beginner in Objective-C. When I try to create a local notification, I save an object of a custom class to the userInfo property of NSUserNotification, but the userInfo seems to be assigned to a nil value.
I will provide a simple code below to demonstrate this.
#import <Cocoa/Cocoa.h>
#interface MyData : NSObject<NSSecureCoding>
#end
#implementation MyData{
double data;
}
-(instancetype)initWithData:(double)data{
[super init];
self->data = data;
return self;
}
+(BOOL)supportsSecureCoding{
return YES;
}
-(void)encodeWithCoder:(nonnull NSCoder *)coder {
[coder encodeDouble:data forKey:#"data"];
}
-(nullable instancetype)initWithCoder:(nonnull NSCoder *)coder {
if (self = [super init]){
self->data = [coder decodeDoubleForKey:#"data"];
}
return self;
}
#end
int main(int argc, const char * argv[]) {
NSUserNotification* n = [NSUserNotification new];
MyData* myData = [[MyData alloc] initWithData: 1.0];
n.userInfo = #{#"MyData":myData};
assert(n.userInfo == nil);// nil userInfo
n.userInfo = #{#"MyData":#"Other"};
assert(n.userInfo != nil);// but not nil here.
return 0;
}
// Why? How can I pass my custom object?
Let's see the documentation of userInfo:
All items must be property list types or an exception is thrown.
Does MyData is one of the property list types? No. The allowed types are listed here.
Now, let's do some test:
NSUserNotification* n = [NSUserNotification new];
NSString *errorDescription = nil;
MyData* myData = [[MyData alloc] initWithData: 1.0];
// String: String
NSDictionary *basicDict = #{#"MyData": #"Other"};
n.userInfo = basicDict;
NSLog(#"UserInfo Basic: %#", n.userInfo);
id plistStrTest = [NSPropertyListSerialization dataFromPropertyList:basicDict
format:NSPropertyListXMLFormat_v1_0
errorDescription:&errorDescription];
if (errorDescription) {
NSLog(#"Error while serializing: %#", errorDescription);
}
NSLog(#"Plist Basic: %#", [[NSString alloc] initWithData:plistStrTest encoding:NSUTF8StringEncoding]);
// String: MyData
NSDictionary *rawDataDict = #{#"MyData": myData};
n.userInfo = rawDataDict;
NSLog(#"UserInfo Data: %#", n.userInfo);
errorDescription = nil;
id plistRawData = [NSPropertyListSerialization dataFromPropertyList:rawDataDict
format:NSPropertyListXMLFormat_v1_0
errorDescription:&errorDescription];
if (errorDescription) {
NSLog(#"Error while serializing: %#", errorDescription);
}
NSLog(#"Plist Raw: %#", [[NSString alloc] initWithData:plistRawData encoding:NSUTF8StringEncoding]);
NSDictionary *combinedRawAndBasic = #{#"MyData":#"Other", #"MyData2": myData};
n.userInfo = combinedRawAndBasic;
NSLog(#"UserInfo Combined: %#", n.userInfo);
id plistCombined = [NSPropertyListSerialization dataFromPropertyList:combinedRawAndBasic
format:NSPropertyListXMLFormat_v1_0
errorDescription:&errorDescription];
if (errorDescription) {
NSLog(#"Error while serializing: %#", errorDescription);
}
NSLog(#"Plist Combined: %#", [[NSString alloc] initWithData:plistCombined encoding:NSUTF8StringEncoding]);
// With Encoding, i.e with NSData
NSError *encodingError = nil;
NSData *encodedData = [NSKeyedArchiver archivedDataWithRootObject:myData requiringSecureCoding:YES error:&encodingError];
if (encodingError) {
NSLog(#"Error while encoding: %#", encodingError);
}
NSDictionary *encodedDict = #{#"MyData": #"Other", #"MyData2": encodedData};
n.userInfo = encodedDict;
NSLog(#"UserInfo encoded: %#", n.userInfo);
errorDescription = nil;
id plistEncodedData = [NSPropertyListSerialization dataFromPropertyList:encodedDict
format:NSPropertyListXMLFormat_v1_0
errorDescription:&errorDescription];
if (errorDescription) {
NSLog(#"Error while serializing: %#", errorDescription);
}
NSLog(#"Plist: %#", [[NSString alloc] initWithData:plistEncodedData encoding:NSUTF8StringEncoding]);
You'll see then, and it concords with the documentation that if you put directly a MyData object, it will reject all the dictionary. But not if you encode it first with NSKeyedArchiver.
Strangely, since the doc talks about an exception thrown, I would have a expect a NSUncaughtException in console and a crash, but there isn't, it's a silent one, that just reject the userInfo set.
You'll need later NSKeyedUnarchiver to decode the encoded data.

Why do I get Use of undeclared identifier 'downloadDataFromURL' when method is defined in same class?

I have written a method that I want to reuse from another method in the same class but I am getting this error message and I don't understand why, how can it be undeclared when it is declared in the same class?
the h file looks like this
#import <Foundation/Foundation.h>
#import "AppDelegate.h"
#import "NWTillHelper.h"
#interface JoshuaWebServices : NSObject
+ (void)downloadDataFromURL:(NSURL *)url withCompletionHandler:(void (^)(NSData *))completionHandler;
- (void)downloadCollections;
#end
And the m file as follows
#import "JoshuaWebServices.h"
#implementation JoshuaWebServices
#synthesize xmlParser;
+ (void)downloadDataFromURL:(NSURL *)url withCompletionHandler:(void (^)(NSData *))completionHandler {
if([NWTillHelper isDebug] == 1) {
NSLog(#"%s entered", __PRETTY_FUNCTION__);
}
// Lots of irrelevant code here
}
- (void)downloadCollections {
// Prepare the URL that we'll get the neighbour countries from.
NSString *URLString = [NSString stringWithFormat:#"https://url.is.not.here"];
NSURL *url = [NSURL URLWithString:URLString];
// Download the data.
[downloadDataFromURL:url withCompletionHandler:^(NSData *data) {
// Make sure that there is data.
if (data != nil) {
self.xmlParser = [[NSXMLParser alloc] initWithData:data];
self.xmlParser.delegate = self;
// Initialize the mutable string that we'll use during parsing.
self.foundValue = [[NSMutableString alloc] init];
// Start parsing.
[self.xmlParser parse];
}
}];
}
Why can I not use the method declared in same class?
Your method needs a receiver. Unlike functions that can just be called on there own. Methods must be called by something, either the class, or an instance of a class. In your case you should use a class because it's a class method.
Change
[downloadDataFromURL:url withCompletionHandler:^(NSData *data) {
// Make sure that there is data.
if (data != nil) {
self.xmlParser = [[NSXMLParser alloc] initWithData:data];
self.xmlParser.delegate = self;
// Initialize the mutable string that we'll use during parsing.
self.foundValue = [[NSMutableString alloc] init];
// Start parsing.
[self.xmlParser parse];
}
}];
to be
[JoshuaWebServices downloadDataFromURL:url withCompletionHandler:^(NSData *data) {
// Make sure that there is data.
if (data != nil) {
self.xmlParser = [[NSXMLParser alloc] initWithData:data];
self.xmlParser.delegate = self;
// Initialize the mutable string that we'll use during parsing.
self.foundValue = [[NSMutableString alloc] init];
// Start parsing.
[self.xmlParser parse];
}
}];

EXC_BAD_ACCESS for an object created inside a Block

I have always been nervous when it comes to blocks and GCD because my mind tells me that it looks very complex!
I am getting a crash inside a block which ideally looks alright to me:
#pragma mark -
-(void)fetchOrdersListWithInfoDict:(NSDictionary*)infoDict completionBlock:(CompletionBlock)completionBlock errorBlock:(ErrorBlock)errorBlock
{
__weak VTVehicleServiceNetworkManager *weakSelf = self;
TaskBlock fetchOrdersListTaskBlock = ^()
{
__block __strong HLOrdersDataProvider *ordersDataProvider = nil;
NSBlockOperation *fetchOrdersOperation = [NSBlockOperation blockOperationWithBlock:[^{
ordersDataProvider = [[HLOrdersDataProvider alloc] init];
[ordersDataProvider performFetchOrdersListWithInfoDict:infoDict
completionBlock:completionBlock
errorBlock:errorBlock];
} copy]];
[weakSelf.dataOperationQueue addOperation:fetchOrdersOperation];
};
[self fetchDataWithTaskBlock:[fetchOrdersListTaskBlock copy]
errorBlock:^(NSError *error) {
errorBlock(error);
}];
}
I was able to trace out the zombie object but I am not able to figure out why is this object turning out to be a zombie. Here is the snapshot from profile:
I have gone through the following guides (1, 2) to see if I can find out what I am doing wrong but I was no where near to find out what is going wrong.
Any help and reference text to what I am doing wrong will help.
Edit:
I have tried what #Jerimy has suggested and in fact my code which I have posted earlier was exactly the same as required: Declaring and initializing ordersDataProvider inside the block operation itself. But since it was crashing at the same point I tried to declare it outside the block just to see if it addresses the crash.
Below is the new code I tested:
#pragma mark -
-(void)fetchOrdersListWithInfoDict:(NSDictionary*)infoDict completionBlock:(CompletionBlock)completionBlock errorBlock:(ErrorBlock)errorBlock
{
__weak VTVehicleServiceNetworkManager *weakSelf = self;
completionBlock = [completionBlock copy];
errorBlock = [errorBlock copy];
TaskBlock fetchOrdersListTaskBlock = ^()
{
NSBlockOperation *fetchOrdersOperation = [NSBlockOperation blockOperationWithBlock:^{
HLOrdersDataProvider *ordersDataProvider = [[HLOrdersDataProvider alloc] init];
[ordersDataProvider performFetchOrdersListWithInfoDict:infoDict
completionBlock:completionBlock
errorBlock:errorBlock];
}];
[weakSelf.dataOperationQueue addOperation:fetchOrdersOperation];
};
[self fetchDataWithTaskBlock:[fetchOrdersListTaskBlock copy]
errorBlock:^(NSError *error) {
errorBlock(error);
}];
}
The crash from Profile:
There is not much from the stack trace as well, SDMHTTPRequest is a library and am very sure there is nothing wrong there, and the HLOrdersDataProvider is the zombie object which I was able to trace out in Instruments app:
EDIT 2
Adding the interface and implementation of HLOrdersDataProvider for more details:
#interface HLOrdersDataProvider : HLDataProvider
-(void)performFetchOrdersListWithInfoDict:(NSDictionary*)infoDict completionBlock:(CompletionBlock)completionBlock errorBlock:(ErrorBlock)errorBlock;
#end
#implementation HLOrdersDataProvider
-(void)performFetchOrdersListWithInfoDict:(NSDictionary*)infoDict completionBlock:(CompletionBlock)completionBlock errorBlock:(ErrorBlock)errorBlock
{
// Using SDMConnectivity
NSString *queryString = [infoDict valueForKey:#"$filter"];
NSString *appendStringForEndpoint = [kRowsetsKeyword stringByAppendingFormat:#"?%#", queryString];
[self fetchDataFromServerWithEndPointAppendString:appendStringForEndpoint
completionBlock:completionBlock
errorBlock:errorBlock];
}
#pragma mark - Service Agent related
-(NSString*)collectionName
{
return [NSString stringWithString:kRowsetsKeyword];
}
-(void)requestFinished:(SDMHttpRequest *)request
{
NSError *error = nil;
// Let's parse the response and send the results back to the caller
NSString *collectionName = [self collectionName];
NSData *responseData = [request responseData];
NSArray *entitiesArray = [self parseODataEntriesWithData:responseData
withCollectionName:collectionName
error:&error];
if (error)
[self triggerFailureBlockWithArgument:error];
else
[self triggerCompletionBlockWithArgument:entitiesArray];
}
#end
Also, HLOrdersDataProvider is inherited from HLDataProvider so below is the interface and implementation of this class too:
#import <Foundation/Foundation.h>
//#import "SDMHttpRequestDelegate.h"
#import "SDMRequestBuilder.h"
#import "SDMHttpRequest.h"
#import "SDMParser.h"
#import "HLConstant.h"
#import "HLConnectionData.h"
#interface HLDataProvider : NSObject <SDMHttpRequestDelegate>
#property (copy, atomic) CompletionBlock completionBlock;
#property (copy , atomic) ErrorBlock errorBlock;
#property (copy, atomic) CompletionBlockWithDataFetchStatus completionBlockWithDataFetchStatus;
+ (id)sharedInstance;
- (NSMutableArray*)parseODataEntriesWithData:(NSData*)data withCollectionName:(NSString*)collectionName;
- (NSMutableArray*)parseODataEntriesWithData:(NSData*)data withCollectionName:(NSString*)collectionName error:(NSError**)outError;
- (NSMutableArray*)parseJSONEntriesWithData:(NSData*)data;
-(NSArray*)fetchEntriesFromDatabaseWithEntityName:(NSString*)entityName relationshipObjects:(NSMutableArray*)relationships;
-(void)updateDatabaseWithEntries:(NSMutableArray*)scanEntries;
-(void)updateDatabaseWithJSONEntries:(NSMutableArray*)scanEntries;
-(id)parsedOdataResultFromEntries:(NSMutableArray*)entries;
-(void)fetchDataFromServerWithEndPointAppendString:(NSString*)appendStr completionBlock:(CompletionBlock)inCompletionBlock errorBlock:(ErrorBlock)inErrorBlock;
-(NSString*)collectionName;
-(void)triggerCompletionBlockWithArgument:(id)inArg;
-(void)triggerFailureBlockWithArgument:(NSError*)inArg;
#end
#implementation HLDataProvider
+ (id)sharedInstance
{
//Subclassess will override this method
return nil;
}
-(NSMutableArray*)parseODataEntriesWithData:(NSData*)data withCollectionName:(NSString*)collectionName
{
return [self parseODataEntriesWithData:data
withCollectionName:collectionName
error:NULL];
}
-(NSMutableArray*)parseODataEntriesWithData:(NSData*)data withCollectionName:(NSString*)collectionName error:(NSError**)outError
{
NSMutableArray *entriesArray = nil;
#try {
entriesArray = sdmParseODataEntriesXML(data,
[[[HLConnectionData metaDataDocument] getCollectionByName:collectionName] getEntitySchema],
[HLConnectionData serviceDocument]);
}
#catch (NSException *exception) {
NSLog(#"Got exception: %#", exception);
if (outError)
{
*outError = [NSError errorWithDomain:#"Vehicle Service"
code:-1001
userInfo:[NSDictionary dictionaryWithObject:exception forKey:NSLocalizedDescriptionKey]];
}
}
#finally {
}
return entriesArray;
}
- (NSMutableArray*)parseJSONEntriesWithData:(NSData*)data
{
NSError *error = nil;
id object = [NSJSONSerialization
JSONObjectWithData:data
options:0
error:&error];
NSMutableArray *resultArray = nil;
if(error) { /* JSON was malformed, act appropriately here */ }
if([object isKindOfClass:[NSDictionary class]])
{
resultArray = [NSMutableArray arrayWithObject:object];
}
else if ([object isKindOfClass:[NSArray class]])
{
resultArray = [NSMutableArray arrayWithArray:object];
}
return resultArray;
}
#pragma mark -
#pragma mark - Data Fetch - Server - SDMConnectivity
-(void)fetchDataFromServerWithEndPointAppendString:(NSString*)appendStr completionBlock:(CompletionBlock)inCompletionBlock errorBlock:(ErrorBlock)inErrorBlock;
{
self.errorBlock = inErrorBlock;
self.completionBlock = inCompletionBlock;
id<SDMRequesting> request = nil;
//NSString *clientStr = #"&sap-client=320&sap-language=EN";
NSString *urlStr =[NSString stringWithFormat:#"%#/%#",[HLConnectionData applicationEndPoint], appendStr];
urlStr = [urlStr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
[SDMRequestBuilder setRequestType:HTTPRequestType];
request=[SDMRequestBuilder requestWithURL:[NSURL URLWithString:urlStr]];
[request setUsername:kUserName];
/*Set Password in SDMRequesting object*/
[request setPassword:kPassword];
[request setRequestMethod:#"GET"];
[request setTimeOutSeconds:kTimeoutInterval];
/*set the Delegate. This class must adhere to SDMHttpRequestDelegate to get the callback*/
[request setDelegate:self];
/*Call startAsynchronous API to request object to retreive Data asynchrnously in the call backs */
[request startSynchronous];
}
-(void)updateDatabaseWithEntries:(NSMutableArray*)scanEntries
{
//Subclasses will override this
}
-(void)updateDatabaseWithJSONEntries:(NSMutableArray*)scanEntries
{
//Subclasses will override this
}
-(id)parsedOdataResultFromEntries:(NSMutableArray*)entries
{
//Subclasses will override this
return nil;
}
-(void)deleteExistingEntriesFromCoredata
{
//Subclasses will override this
}
-(NSArray*)fetchEntriesFromDatabaseWithEntityName:(NSString*)entityName relationshipObjects:(NSMutableArray*)array
{
//Subclasses will overide this method
return nil;
}
#pragma mark - SDMHTTPRequestDelegate methods
- (void)requestStarted:(SDMHttpRequest*) request
{
}
- (void)requestFinished:(SDMHttpRequest*) request
{
// For service doc and metadata we instantiate HLDataProvider, so we send this raw SDMHTTPRequest object as-is. For other service agents like HLOrdersDataProvider we send the parsed information, check the subclass' implementation of -requestFinished: method
[self triggerCompletionBlockWithArgument:request];
}
-(void)triggerCompletionBlockWithArgument:(id)inArg
{
self.completionBlock(inArg);
}
- (void)requestFailed:(SDMHttpRequest*) request
{
[self triggerFailureBlockWithArgument:request.error];
}
-(void)triggerFailureBlockWithArgument:(NSError*)inArg
{
self.errorBlock(inArg);
}
- (void)requestRedirected:(SDMHttpRequest*) request
{
}
#pragma mark - Service Agent related
-(NSString*)collectionName
{
// Should be overridden by the subclasses
return nil;
}
The referenced code is very convoluted (you're doing things in a very complicated way).
First off, you should not be creating the copy of your blocks in the caller. Make the copy in the callee if necessary (ie: to copy it to heap instead of using the stack-allocated block if you are going to call it after the stack has been popped). In almost all APIs using blocks, it is not the caller's responsibility to ensure that a block is on heap.
There is no reason for your ordersDataProvider variable to be declared in the scope of fetchOrdersListTaskBlock because it is only ever used inside of fetchOrdersOperation's block.
I don't immediately see the cause of your crash, but I suspect that simplifying your code will help reveal the problem. Perhaps the issue is in HLOrdersDataProvider's initializer.
Try something like:
-(void)fetchOrdersListWithInfoDict:(NSDictionary*)infoDict completionBlock:(CompletionBlock)completionBlock errorBlock:(ErrorBlock)errorBlock
{
completionBlock = [completionBlock copy];
errorBlock = [errorBlock copy];
__weak VTVehicleServiceNetworkManager *weakSelf = self;
TaskBlock fetchOrdersListTaskBlock = ^{
NSBlockOperation *fetchOrdersOperation = [NSBlockOperation blockOperationWithBlock:^{
HLOrdersDataProvider *ordersDataProvider = [[HLOrdersDataProvider alloc] init];
[ordersDataProvider performFetchOrdersListWithInfoDict:infoDict
completionBlock:completionBlock
errorBlock:errorBlock];
}];
[weakSelf.dataOperationQueue addOperation:fetchOrdersOperation];
};
[self fetchDataWithTaskBlock:fetchOrdersListTaskBlock
errorBlock:errorBlock];
}
Or better yet, re-design your class to work like this (I don't see a need for NSBlockOperation in your example):
-(void)fetchOrdersListWithInfoDict:(NSDictionary*)infoDict completionBlock:(CompletionBlock)completionBlock errorBlock:(ErrorBlock)errorBlock
{
completionBlock = [completionBlock copy];
errorBlock = [errorBlock copy];
TaskBlock fetchOrdersListTaskBlock = ^{
HLOrdersDataProvider *ordersDataProvider = [[HLOrdersDataProvider alloc] init];
[ordersDataProvider performFetchOrdersListWithInfoDict:infoDict
completionBlock:completionBlock
errorBlock:errorBlock];
};
[self fetchDataWithTaskBlock:fetchOrdersListTaskBlock
errorBlock:errorBlock];
}

Objective-C crash with zombie object

I've made an implementation for an JSON-RPC (a little bit modified) Server/Client in objective-c with the GCDAsyncSocket library. but the app crashes on responding to an request. without debugging for zombies i'm getting this error:
JSONRPCTestServer(1301,0x7fff7f887960) malloc: *** error for object 0x10014db10: pointer being freed was not allocated
*** set a breakpoint in malloc_error_break to debug
the screenshot in xcode shows the error is in the completeCurrentRead method of the GCDAsyncSocket library.
when debugging for zombies it logs this:
2013-02-04 14:36:16.430 JSONRPCTestServer[1367:603] *** -[__NSArrayI release]: message sent to deallocated instance 0x1005b6fd0
and instruments shows this:
as this happens when a response to the rpc-call is of type nsarray i'd guess its this array that is causing the error. the rpc-method is:
-(NSArray *)testArray:(GCDAsyncSocket *)sock {
return [NSArray arrayWithObjects:#"test1",#"test2", nil];
}
The JSON-RPC header is:
#import <Foundation/Foundation.h>
#import "JSONRPCMethod.h"
#import "JSONRPCResponse.h"
#import "JSONRPCError.h"
#import "JSONRPCArgument.h"
#import "JSONRPCRequest.h"
#import "GCDAsyncSocket.h"
#class GCDAsyncSocket;
#protocol JSONRPCResponseDelegate <NSObject>
#optional
-(void)rpcSocket:(GCDAsyncSocket*)sock returnedValue:(id)retVal forMethod:(NSString*)m id:(id)i;
-(void)rpcSocket:(GCDAsyncSocket*)sock returnedError:(JSONRPCError*)err forMethod:(NSString*)m id:(id)i;
-(void)rpcReturnedValue:(id)retVal forMethod:(NSString*)m id:(id)i;
-(void)rpcReturnedError:(JSONRPCError*)err forMethod:(NSString*)m id:(id)i;
#end
#interface JSONRPC : NSObject {
NSMutableArray *supportedMethods;
GCDAsyncSocket *mainSocket;
NSMutableArray *connectedSockets;
NSMutableArray *responseDelegates;
BOOL isServer;
}
+(JSONRPC*)sharedConnection;
-(BOOL)startServer:(NSUInteger)port;
-(BOOL)connectToServer:(NSString*)host port:(NSUInteger)port;
-(BOOL)addMethod:(JSONRPCMethod*)method;
-(void)removeMethod:(JSONRPCMethod*)method;
-(void)removeMethodsWithTarget:(id)target;
-(void)sendRequest:(JSONRPCRequest*)req toSocket:(GCDAsyncSocket*)sock;
-(void)sendRequest:(JSONRPCRequest*)req;
-(void)sendNotification:(JSONRPCRequest*)req toSocket:(GCDAsyncSocket*)sock;
-(void)sendNotification:(JSONRPCRequest*)req;
-(void)sendNotification:(JSONRPCRequest*)req toSocketsWithUserData:(id)userData;
#end
.m is:
#import "JSONRPC.h"
#import "GCDAsyncSocket.h"
#define kGeneralReadTimeout -1.0
#define kGeneralWriteTimeout -1.0
#implementation JSONRPC
- (id)init
{
self = [super init];
if (self) {
isServer = NO;
supportedMethods = [[NSMutableArray alloc] init];
mainSocket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_main_queue()];
connectedSockets = [[NSMutableArray alloc] init];
responseDelegates = [[NSMutableArray alloc] init];
}
return self;
}
+ (JSONRPC *)sharedConnection {
static JSONRPC *sharedSingleton;
#synchronized(self)
{
if (!sharedSingleton)
sharedSingleton = [[JSONRPC alloc] init];
return sharedSingleton;
}
}
-(BOOL)startServer:(NSUInteger)port {
// Now we tell the socket to accept incoming connections.
// We don't care what port it listens on, so we pass zero for the port number.
// This allows the operating system to automatically assign us an available port.
isServer = YES;
NSError *err = nil;
if ([mainSocket acceptOnPort:port error:&err]) {
} else {
DDLogError(#"Error while starting JSON-RPC Server: %#",err);
return NO;
}
DDLogInfo(#"Started JSON-RPC Server on port %hu",[mainSocket localPort]);
return YES;
}
-(BOOL)connectToServer:(NSString *)host port:(NSUInteger)port {
NSError *err = nil;
mainSocket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_main_queue()];
[mainSocket connectToHost:host onPort:port error:&err];
if(err != nil) {
DDLogError(#"Couldn't connect to host %#:%lu (Error: %#)",host,port,err);
return NO;
}
return YES;
}
-(BOOL)addMethod:(JSONRPCMethod *)method {
for (JSONRPCMethod *meth in supportedMethods) {
if([meth.name isEqualToString:method.name]) {
return NO;
}
}
[supportedMethods addObject:method];
return YES;
}
-(void)removeMethod:(JSONRPCMethod *)method {
[supportedMethods removeObject:method];
}
-(void)removeMethodsWithTarget:(id)target {
NSMutableArray *toRemove = [[NSMutableArray alloc] init];
for (JSONRPCMethod *meth in supportedMethods) {
if(meth.target == target) {
[toRemove addObject:meth];
}
}
[supportedMethods removeObjectsInArray:toRemove];
}
-(void)sendRequest:(JSONRPCRequest *)req toSocket:(GCDAsyncSocket*)sock {
[responseDelegates addObject:req];
[req setIdentifier:[NSNumber numberWithUnsignedInteger:[responseDelegates count]-1]];
[self sendPackage:[req dictionary] toSocket:sock];
}
-(void)sendRequest:(JSONRPCRequest *)req {
[self sendRequest:req toSocket:mainSocket];
}
-(void)sendNotification:(JSONRPCRequest *)req toSocket:(GCDAsyncSocket*)sock{
[req setIdentifier:nil];
[self sendPackage:[req dictionary] toSocket:sock];
}
-(void)sendNotification:(JSONRPCRequest *)req {
[self sendNotification:req toSocket:mainSocket];
}
-(void)sendNotification:(JSONRPCRequest *)req toSocketsWithUserData:(id)userData {
NSMutableArray *matchingSockets = [[NSMutableArray alloc] init];
for (GCDAsyncSocket*sock in connectedSockets) {
if(sock.userData == userData) {
[matchingSockets addObject:sock];
}
}
if(matchingSockets.count == 0)
return;
[req setIdentifier:nil];
NSData *pkgData = [self writableDataFromDictionary:[req dictionary]];
for (GCDAsyncSocket*sock in matchingSockets) {
[sock writeData:pkgData withTimeout:kGeneralWriteTimeout tag:0];
}
}
#pragma mark Socket Delegate
- (void)socket:(GCDAsyncSocket *)sock didAcceptNewSocket:(GCDAsyncSocket *)newSocket {
[connectedSockets addObject:newSocket];
[newSocket readDataToData:[GCDAsyncSocket ZeroData] withTimeout:kGeneralReadTimeout tag:0];
}
- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(UInt16)port {
DDLogVerbose(#"socket:didConnectToHost:%# port:%hu", host, port);
[sock readDataToData:[GCDAsyncSocket ZeroData] withTimeout:kGeneralReadTimeout tag:0];
}
- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err {
DDLogVerbose(#"socketDidDisconnect:%#", err);
if(isServer)
[connectedSockets removeObject:sock];
}
-(void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag {
// So, we've received something from the client
// As we have to cut out the last 0x00 for JSONSerialization it has to be longer than 1 byte
if(data.length > 1) {
// Shorten out that 0x00
data = [data subdataWithRange:NSMakeRange(0, data.length-1)];
// Try to serialize
NSError *err;
NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:data options:0 error:&err];
DDLogVerbose(#"Dict: %#",dict);
if(err != nil) {
// The package isn't json
JSONRPCResponse *response = [JSONRPCResponse responseWithError:[JSONRPCError invalidRequest]];
[self sendPackage:[response dictionary] toSocket:sock];
} else {
JSONRPCResponse *response = [self handleDictionary:dict fromSocket:sock];
if(response != nil) {
[self sendPackage:[response dictionary] toSocket:sock];
}
}
}
[sock readDataToData:[GCDAsyncSocket ZeroData] withTimeout:kGeneralReadTimeout tag:0];
}
-(JSONRPCResponse*)handleDictionary:(NSDictionary*)dict fromSocket:(GCDAsyncSocket*)sock {
// Check if the "id" is of a correct value/type
id identifier = [dict valueForKey:#"id"];
if(!(identifier == nil || [identifier isKindOfClass:[NSNumber class]])) {
return [JSONRPCResponse responseWithError:[JSONRPCError invalidRequest]];
}
// Handle the package
NSString *methodName = [dict valueForKey:#"method"];
id errorValue = [dict valueForKey:#"error"];
id resultValue = [dict valueForKey:#"result"];
if([methodName isKindOfClass:[NSString class]]) {
// We have a string as method
DDLogInfo(#"Method: %#, object: %#",methodName,[dict valueForKey:#"params"]);
for (JSONRPCMethod *method in supportedMethods) {
if([method.name isEqualToString:methodName]) {
id result = nil;
if(isServer == YES) {
// It is a server and the method needs to know from where the call comes
result = [method invoke:[dict valueForKey:#"params"] fromSocket:sock];
} else {
// It is a client and we don't need to know where the call is from. it can only be the server.
result = [method invoke:[dict valueForKey:#"params"]];
}
if([result isKindOfClass:[JSONRPCError class]]) {
return [JSONRPCResponse responseWithError:result id:identifier];
} else if(result != nil) {
return [JSONRPCResponse responseWithResult:result id:identifier];
} else {
return nil;
}
}
}
} else if(resultValue != nil) {
// We have a response from our partner
//DDLogInfo(#"Result: %#",resultValue);
NSUInteger responseDelegateId = [identifier unsignedIntegerValue];
if(responseDelegateId < [responseDelegates count]) {
JSONRPCRequest *originalRequest = [responseDelegates objectAtIndex:responseDelegateId];
if(originalRequest.sender == nil) {
return nil;
}
#try {
SEL selector;
if(isServer) {
selector = #selector(rpcSocket:returnedValue:forMethod:id:);
} else {
selector = #selector(rpcReturnedValue:forMethod:id:);
}
NSMethodSignature *signature = [originalRequest.sender methodSignatureForSelector:selector];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
[invocation setTarget:originalRequest.sender];
[invocation setSelector:selector];
NSUInteger startArg = 2;
if(isServer) {
[invocation setArgument:&sock atIndex:startArg];
startArg++;
}
NSString *method = [originalRequest method];
id orgId = [originalRequest identifier];
[invocation setArgument:&resultValue atIndex:startArg];
[invocation setArgument:&method atIndex:startArg+1];
[invocation setArgument:&orgId atIndex:startArg+2];
[invocation invoke];
}
#catch (NSException *exception) {
DDLogWarn(#"Couldn't find a response: %#",exception);
}
}
} else if(errorValue != nil) {
// We have a string as method
DDLogInfo(#"Error: %#",errorValue);
} else {
return [JSONRPCResponse responseWithError:[JSONRPCError invalidRequest] id:identifier];
}
return nil;
}
-(void)sendPackage:(NSDictionary *)dict toSocket:(GCDAsyncSocket *)sock {
NSData *answerData = [self writableDataFromDictionary:dict];
[sock writeData:answerData withTimeout:kGeneralWriteTimeout tag:0];
}
-(NSData*)writableDataFromDictionary:(NSDictionary*)dict {
NSMutableData *answerData = [[NSMutableData alloc] init];
// Serialize the answer
NSError *err = nil;
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:dict options:0 error:&err];
if(err != nil) {
// Log
DDLogError(#"JSON-RPC had an internal error while converting the answer to JSON. The answer-dictionary is: %#",dict);
// Form answer manually
jsonData = [NSData dataWithBytes:"{\"error\":{\"code\":-32700,\"message\":\"Parse error\"}}"
length:49];
}
// Format the answer
[answerData appendData:jsonData];
[answerData appendData:[GCDAsyncSocket ZeroData]];
return answerData;
}
i just don't know how to fix this. why do i keep getting an error with an nsarray but when i return a nsnumber it works? how can i fix this?

Objective-C: How to add query parameter to NSURL?

Let's say I have an NSURL? Whether or not it already has an empty query string, how do I add one or more parameters to the query of the NSURL? I.e., does anyone know of an implementation of this function?
- (NSURL *)URLByAppendingQueryString:(NSString *)queryString
So that it satisfies this NSURL+AdditionsSpec.h file:
#import "NSURL+Additions.h"
#import "Kiwi.h"
SPEC_BEGIN(NSURL_AdditionsSpec)
describe(#"NSURL+Additions", ^{
__block NSURL *aURL;
beforeEach(^{
aURL = [[NSURL alloc] initWithString:#"http://www.example.com"];
aURLWithQuery = [[NSURL alloc] initWithString:#"http://www.example.com?key=value"];
});
afterEach(^{
[aURL release];
[aURLWithQuery release];
});
describe(#"-URLByAppendingQueryString:", ^{
it(#"adds to plain URL", ^{
[[[[aURL URLByAppendingQueryString:#"key=value&key2=value2"] query] should]
equal:#"key=value&key2=value2"];
});
it(#"appends to the existing query sting", ^{
[[[[aURLWithQuery URLByAppendingQueryString:#"key2=value2&key3=value3"] query] should]
equal:#"key=value&key2=value2&key3=value3"];
});
});
});
SPEC_END
Since iOS 7 you can use NSURLComponents that is very simple to use. Take a look on these examples:
Example 1
NSString *urlString = #"https://mail.google.com/mail/u/0/?shva=1#inbox";
NSURLComponents *components = [[NSURLComponents alloc] initWithString:urlString];
NSLog(#"%# - %# - %# - %#", components.scheme, components.host, components.query, components.fragment);
Example 2
NSString *urlString = #"https://mail.google.com/mail/u/0/?shva=1#inbox";
NSURLComponents *components = [[NSURLComponents alloc] initWithString:urlString];
if (components) {
//good URL
} else {
//bad URL
}
Example 3
NSURLComponents *components = [NSURLComponents new];
[components setScheme:#"https"];
[components setHost:#"mail.google.com"];
[components setQuery:#"shva=1"];
[components setFragment:#"inbox"];
[components setPath:#"/mail/u/0/"];
[self.webview loadRequest:[[NSURLRequest alloc] initWithURL:[components URL]]];
But you can do many other things with NSURLComponents take a look on NSURLComponents class reference on Apple documentation or on this link: http://nshipster.com/nsurl/
Here's an implementation that passes your specs:
#implementation NSURL (Additions)
- (NSURL *)URLByAppendingQueryString:(NSString *)queryString {
if (![queryString length]) {
return self;
}
NSString *URLString = [[NSString alloc] initWithFormat:#"%#%#%#", [self absoluteString],
[self query] ? #"&" : #"?", queryString];
NSURL *theURL = [NSURL URLWithString:URLString];
[URLString release];
return theURL;
}
#end
And here is an implementation for NSString:
#implementation NSString (Additions)
- (NSURL *)URLByAppendingQueryString:(NSString *)queryString {
if (![queryString length]) {
return [NSURL URLWithString:self];
}
NSString *URLString = [[NSString alloc] initWithFormat:#"%#%#%#", self,
[self rangeOfString:#"?"].length > 0 ? #"&" : #"?", queryString];
NSURL *theURL = [NSURL URLWithString:URLString];
[URLString release];
return theURL;
}
// Or:
- (NSString *)URLStringByAppendingQueryString:(NSString *)queryString {
if (![queryString length]) {
return self;
}
return [NSString stringWithFormat:#"%#%#%#", self,
[self rangeOfString:#"?"].length > 0 ? #"&" : #"?", queryString];
}
#end
The iOS8+ modern way
adding (or replacing 'ref' value if exists) ref=impm to url which is on min60.com
if ([[url host] hasSuffix:#"min60.com"]) {
NSURLComponents *components = [[NSURLComponents alloc] initWithURL:url resolvingAgainstBaseURL:NO];
NSURLQueryItem * newQueryItem = [[NSURLQueryItem alloc] initWithName:#"ref" value:#"impm"];
NSMutableArray * newQueryItems = [NSMutableArray arrayWithCapacity:[components.queryItems count] + 1];
for (NSURLQueryItem * qi in components.queryItems) {
if (![qi.name isEqual:newQueryItem.name]) {
[newQueryItems addObject:qi];
}
}
[newQueryItems addObject:newQueryItem];
[components setQueryItems:newQueryItems];
url = [components URL];
}
Just a friendly post for those who don't want to write boilerplate code while building NSURL with NSURLComponents.
Since iOS8 we have NSURLQueryItem that helps building URL request freaking fast.
I wrote a little handy category to ease the work, that you can grab here: URLQueryBuilder
Here is example of how easy it is to work with it:
NSString *baseURL = #"https://google.com/search";
NSDictionary *items = #{
#"q" : #"arsenkin.com",
#"hl" : #"en_US",
#"lr" : #"lang_en"
};
NSURL *URL = [NSURL ars_queryWithString:baseURL queryElements:items];
// https://google.com/search?q=arsenkin.com&hl=en_US&lr=lang_en
I have an extension to NSURLComponents that add query item, in swift:
extension NSURLComponents {
func appendQueryItem(name name: String, value: String) {
var queryItems: [NSURLQueryItem] = self.queryItems ?? [NSURLQueryItem]()
queryItems.append(NSURLQueryItem(name: name, value: value))
self.queryItems = queryItems
}
}
To use,
let components = NSURLComponents(string: urlString)!
components.appendQueryItem(name: "key", value: "value")
If you're using RestKit it provides additions to NSString. One of which is:
- (NSString *)stringByAppendingQueryParameters:(NSDictionary *)queryParameters
So you could do:
NSDictionary *shopParams = [NSDictionary dictionaryWithKeysAndObjects:
#"limit",#"20",
#"location",#"latitude,longitude",
nil];
NSString *pathWithQuery = [#"/api/v1/shops.json" stringByAppendingQueryParameters:shopParams]
As others have mentioned, you can use NSURLComponents to construct URLs.
#implementation NSURL (Additions)
- (NSURL *)URLByAppendingQueryParameters:(NSDictionary *)queryParameters
{
NSURLComponents *components = [[NSURLComponents alloc] initWithURL:self resolvingAgainstBaseURL:NO];
NSMutableArray *queryItems = [NSMutableArray array:components.queryItems];
for (NSString *key in [queryParameters allKeys]) {
NSURLQueryItem *queryItem = [[NSURLQueryItem alloc] initWithName:key value:queryParameters[key]];
[queryItems addObject:queryItem];
}
components.queryItems = queryItems;
return [components URL];
}
#end
NSURL is not mutable so you cannot implement this functionality directly based on NSURL. Instead you will have to obtain the string representation of the URL, append your parameters to that and then create a new NSURL.
This does not sound like a good solution. Unless there is a good reason, it is better to work with strings until the last moment and only create an NSURL when you have your fully formed request.