How to maintain proper encapsulation for singleton created with data - ios

I have a singleton networking class as well as a singleton object that needs to persist throughout my app. The singleton is initialized based on data retrieved from a web call, so right now my code works, and I have the following in my singleton networking class:
- (void)initializeObjectWithSuccess:(void (^)(BOOL))success
failure:(void (^)(NSError *error))failure {
[self.HTTPClient postPath:[NSString stringWithFormat:#"users/%#/", [CPUser sharedUser].name parameters:[self createParameters] success:^(AFHTTPRequestOperation *operation, id responseObject) {
id json = [NSJSONSerialization JSONObjectWithData:responseObject
options:NSJSONReadingAllowFragments
error:nil];
[[CPList sharedList] setIdentifier:json[#"id"]];
[[CPList sharedList] setImages:json[#"images"]];
if (success) {
success(YES);
}
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
failure(error);
}];
}
I don't know how to initialize all the properties I need on my singleton CPList without setting them within this method, however I know that this is not proper encapsulation because the CPRequestManager Class should know nothing about the CPList Class

If your issue is that you don't want this class to know the name of CPList and the detail that it's a singleton and that it can access it with +[CPList sharedInstance] then you can just pass in an object that conforms to a protocol. This basically moves the knowledge of the singleton somewhere else
- (void)initializeObjectWithList:(id<CPList>)list
success:(void (^)(BOOL))success
failure:(void (^)(NSError *error))failure;
{
[self.HTTPClient postPath:[NSString stringWithFormat:#"users/%#/", [CPUser sharedUser].name parameters:[self createParameters] success:^(AFHTTPRequestOperation *operation, id responseObject) {
id json = [NSJSONSerialization JSONObjectWithData:responseObject
options:NSJSONReadingAllowFragments
error:nil];
[list setIdentifier:json[#"id"]];
[list setImages:json[#"images"]];
if (success) {
success(YES);
}
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
failure(error);
}];
}
Or you could remove all knowledge that there is a "list" and just have this method return the actual data and then the caller can set it on the list
- (void)initializeObjectWithSuccess:(void (^)(NSString *ID, NSArray *images))success
failure:(void (^)(NSError *error))failure;
{
[self.HTTPClient postPath:[NSString stringWithFormat:#"users/%#/", [CPUser sharedUser].name parameters:[self createParameters] success:^(AFHTTPRequestOperation *operation, id responseObject) {
id json = [NSJSONSerialization JSONObjectWithData:responseObject
options:NSJSONReadingAllowFragments
error:nil];
if (success) {
success(json[#"id"], json[#"images"]);
}
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
failure(error);
}];
}
Without any further context it's hard to suggest structural changes but here's two potential refactoring that might get you thinking about what you coudld do

Related

Method with completion handler

I've created a method in a class:
- (void)getTableData:(NSString *)URL withCompletionHandler:(void (^)(NSString *))handler{
__block NSDictionary *JSON;
[manager POST:urlString parameters:jsonDict success:^(AFHTTPRequestOperation *operation, id responseObject){
JSON = [NSJSONSerialization JSONObjectWithData:responseObject options:NSJSONReadingAllowFragments error:&error];
handler(JSON);
}
failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"error %#",error);
// handle failure
}];
}
and call it in another class by
[ObjOfSecondClass getTableData:BILL withCompletionHandler:^(NSString* returnString)handler{
}];
It shows Expected expression error at handler.
It's an expression error because you are using it in wrong way.
Try this one in viewDidLoad
[ObjOfSecondClass getTableData:BILL withCompletionHandler:^(NSString* returnString){
}];
handler is used is block implementation to return value from where they are called.
Note - replace string to dictionary in block definition because you are getting dictionary from API not string.
Learn block syntax

How can I write completion block with nullable?

When I call this method with nil, the app crashes, but I want to know how to write it with nullable.
CRASH
[KPTaxnoteApiSaveHandler saveEntryWithUuid:uuid completion:nil];
OK
[KPTaxnoteApiSaveHandler saveEntryWithUuid:uuid completion:^(NSError *error) {}];
This is the code.
+ (void)saveEntryWithUuid:(NSString *)uuid completion:(void (^ __nullable)(NSError * _Nullable error))completion {
NSLog(#"saveEntryWithUuid");
Entry *entry = [Entry MR_findFirstByAttribute:#"uuid" withValue:uuid];
NSDictionary *params = #{#"entry[uuid]":entry.uuid};
[KPTaxnoteApiSaveHandler postWithUrl:kApiUrlStringForEntry params:params completion:^(NSError *error) {
if (!error) {
[MagicalRecord saveWithBlock:^(NSManagedObjectContext *localContext) {
Entry *entry = [Entry MR_findFirstByAttribute:#"uuid" withValue:uuid inContext:localContext];
entry.needSave = #NO;
}];
}
completion(error);
}];
+ (void)postWithUrl:(NSString *)urlStr params:(NSDictionary *)params completion:(nullable void (^)(NSError *_Nullable error))completion {
AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
[manager POST:urlStr parameters:params success:^(AFHTTPRequestOperation *operation, id responseObject) {
completion(nil);
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
completion(error);
}];
Where is the crash happening? My first guess is you need to do something like this:
if (completion) {
completion(nil); // Or completion(error);
}
This will handle the case where the completion is nil.

How can I log each request/response using AFHTTPSessionManager?

I'm migrating my project to AFNetworking 2.0. When using AFNetworking 1.0, I wrote code to log each request/response in the console. Here's the code:
-(AFHTTPRequestOperation *)HTTPRequestOperationWithRequest:(NSURLRequest *)request
success:(void (^)(AFHTTPRequestOperation *, id))success
failure:(void (^)(AFHTTPRequestOperation *, NSError *))failure
{
AFHTTPRequestOperation *operation =
[super HTTPRequestOperationWithRequest:request
success:^(AFHTTPRequestOperation *operation, id responseObject){
[self logOperation:operation];
success(operation, responseObject);
}
failure:^(AFHTTPRequestOperation *operation, NSError *error){
failure(operation, error);
}];
return operation;
}
-(void)logOperation:(AFHTTPRequestOperation *)operation {
NSLog(#"Request URL-> %#\n\nRequest Body-> %#\n\nResponse [%d]\n%#\n%#\n\n\n",
operation.request.URL.absoluteString,
[[NSString alloc] initWithData:operation.request.HTTPBody encoding:NSUTF8StringEncoding],
operation.response.statusCode, operation.response.allHeaderFields, operation.responseString);
}
I'm trying to do the same thing using AFNetworking 2.0, which to my understanding, means using a NSURLSessionDataTask object instead of AFHTTPRequestOperation. Here's my shot at it.
-(NSURLSessionDataTask *)dataTaskWithRequest:(NSURLRequest *)request completionHandler:(void (^)(NSURLResponse *, id, NSError *))completionHandler {
NSURLSessionDataTask *task = [super dataTaskWithRequest:request completionHandler:^(NSURLResponse *response, id responseObject, NSError *error){
[self logTask:task];
completionHandler(response, responseObject, error);
}];
return task;
}
-(void)logTask:(NSURLSessionDataTask *)task {
NSString *requestString = task.originalRequest.URL.absoluteString;
NSString *responseString = task.response.URL.absoluteString;
NSLog(#"\n\nRequest - %#\n\nResponse - %#\n\n", requestString, responseString);
}
The dataTaskWithRequest:completionHandler method is successfully intercepting each call, so I think that's the right method to override, but when I try to log the task in the completionHandler, task is nil. Thus getting nulls printed in the console. However a proper task object is still returned from that method. What's happening here? How can I properly log the request/response for each call?
you can use the library AFNetworking/AFNetworkActivityLogger
https://github.com/AFNetworking/AFNetworkActivityLogger
from the doc:
AFNetworkActivityLogger is an extension for AFNetworking 2.0 that logs
network requests as they are sent and received.
usage:
[[AFNetworkActivityLogger sharedLogger] startLogging];
output:
GET http://example.com/foo/bar.json
200 http://example.com/foo/bar.json
using devel logging level you should have responseHeaderFields and responseString too

AFNetworking 2.0 - how to pass response to another class on success from subclassed AFHTTPSessionManager

Beginner ios AFNetworking 2.0 Qns: Having subclassed AFHTTPSessionManager to something like "MyAPIManager" and placed my all my API calls (GET/POST/PUT etc.) in this custom manager class, I'm having problems making use of the response on request success in another class (say class B).
I know I can refactor this and pluck out the POST call portion to class B, so that I can dump the relevant class B methods in the callback, but this would get messy, especially with multiple API calls.
I want to pass this response (e.g. the returned objectId) to another class and right now I'm just using a NSNotification which class B listens for, but this still feels a bit 'hackish' and am wondering if there is a better way to do this.
Currently in MyAPIManager : AFHTTPSessionManager:
- (void) POSTRecordJson:(NSDictionary *)json
{
[self POST:#"classes/Record/" parameters:json success:^(NSURLSessionDataTask *task, id responseObject) {
NSLog(#"Posted JSON: %#", json.description);
if ([responseObject isKindOfClass:[NSDictionary class]]) {
NSLog(#"Response: %#", responseObject);
//Notify objectId received
[[NSNotificationCenter defaultCenter]
postNotificationName:#"ReceivedObjectIdNotification"
object:self
userInfo:responseObject];
}
} failure:^(NSURLSessionDataTask *task, NSError *error) {
NSLog(#"Error: %#", error);
}];
}
And in Class B I've called:
MyApiManager *manager = [MyApiManager sharedInstance];
[manager POSTRecordJson:someJSONdict];
you could do 2 things.. by using a protocol/delegate or a block..
but i, personally, prefers block soo..
first make a block Datatype
typedef void(^SuccessBlock)(id success); example
and add the parameter with the block on it
- (void) POSTRecordJson:(NSDictionary *)json success:(SuccessBlock)success
{
[self POST:#"classes/Record/" parameters:json success:^(NSURLSessionDataTask *task, id responseObject) {
NSLog(#"Posted JSON: %#", json.description);
if ([responseObject isKindOfClass:[NSDictionary class]]) {
NSLog(#"Response: %#", responseObject);
//Notify objectId received
success(responseObject);
}
} failure:^(NSURLSessionDataTask *task, NSError *error) {
NSLog(#"Error: %#", error);
}];
}
and to call the new function..
MyApiManager *manager = [MyApiManager sharedInstance];
[manager POSTRecordJson:someJSONdict success:^(id result){
NSDictionary *dictionary = (NSDictionary *)result;
NSLog(#"response: %#",dictionary)
}];
You would want to pass a completion block into your -POSTRecordJson: method.
For example, you would refactor your method to do the following:
- (void) POSTRecordJson:(NSDictionary *)json completion:(void(^)(BOOL success, id response, NSError *error))completion
{
[self POST:#"classes/Record/" parameters:json success:^(NSURLSessionDataTask *task, id responseObject) {
NSLog(#"Posted JSON: %#", json.description);
if ([responseObject isKindOfClass:[NSDictionary class]])
{
NSLog(#"Response: %#", responseObject);
if (completion) //if completion is NULL, calling it will crash your app so we always check that it is present.
{
completion(YES, responseObject, nil);
}
}
} failure:^(NSURLSessionDataTask *task, NSError *error) {
NSLog(#"Error: %#", error);
if (completion)
{
completion(NO, nil, error);
}
}];
}
You could then handle this implementation like so:
//assuming `manager` and `dictionary` exist.
[manager POSTRecordJson:dictionary completion^(BOOL success, id response, NSError *error) {
if (success)
{
//do something with `response`
}
else
{
//do something with `error`
}
}];
However, if you are a beginner with AFNetworking and you want to adopt a great structure for handling web services, you should check out this excellent blog post.
You can use blocks to send the response back to the class after the response received from the server:
- (void) POSTRecordJson:(NSDictionary *)json response:(void (^)(id response, NSError *error))responseBlock
{
[self POST:#"classes/Record/" parameters:json success:^(NSURLSessionDataTask *task, id responseObject) {
NSLog(#"Posted JSON: %#", json.description);
if ([responseObject isKindOfClass:[NSDictionary class]]) {
NSLog(#"Response: %#", responseObject);
responseBlock(responseObject, nil);
}
} failure:^(NSURLSessionDataTask *task, NSError *error) {
NSLog(#"Error: %#", error);
responseBlock(nil, error);
}];
}

Assign NSArray from a ViewController to go through a client

I have a NSArray in my ViewController called tweets. To retrieve the tweets I have to use a method in my TwitterClient.m class. The method is shown below:
-(NSArray*)getTimeline {
NSArray *timelineArray;
[self.twitterClient registerHTTPOperationClass:[AFJSONRequestOperation class]];
[self.twitterClient getPath:#"1.1/statuses/user_timeline.json" parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) {
NSArray *responseArray = (NSArray *)responseObject;
[responseArray enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
NSLog(#"Success: %#", obj);
}];
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"Error: %#", error);
}];
return responseArray;
}
However as you know you cant return the responseArray because it is inside a block. What would be a more efficient way to do this.
I have another way but in this method I cannot assign the response array as the NSArray that is the parameter:
-(void)getTimeline:(NSArray*)tweetArray {
[self.twitterClient registerHTTPOperationClass:[AFJSONRequestOperation class]];
[self.twitterClient getPath:#"1.1/statuses/user_timeline.json" parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) {
NSArray *responseArray = (NSArray *)responseObject;
[responseArray enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
NSLog(#"Success: %#", obj);
responseArray = tweetArray;
}];
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"Error: %#", error);
}];
}
Basically the final question is how do I assign the tweets array in my View Controller to equal the response array that comes in through the TwitterClient class.
You should pass a block that is getting called after you are done getting your required information:
- (void)getTimelineWithCompletionBlock:(void (^)(NSError *err, NSArray *arr)) block {
[self.twitterClient registerHTTPOperationClass:[AFJSONRequestOperation class]];
[self.twitterClient getPath:#"1.1/statuses/user_timeline.json" parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) {
(block ? block(nil, responseObject) : nil);
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
(block ? block(error, nil) : nil);
}];
}
You can call the method the following way:
[self getTimelineWithCompletionBlock:^(NSError *err, NSArray *arr) {
}];
Inside the completion block you have the data available and you can update your UI or whatever you intend to do. Depending on the implementation of getPath, you maybe have to dispatch the completion block on the main queue, because you are just allowed to update UI on the main thread.

Resources