+ (NSURLSessionDataTask *)login:(NSString*)email andPassword:(NSString*)password andCallback:(void (^)(NSArray *responseArray, NSError *error))block {
if(![self hasInternet]){return nil;}
NSLog(#"Session.login");
[APIClient sharedClient].requestSerializer = [AFJSONRequestSerializer serializer];
[[APIClient sharedClient].requestSerializer setValue:email forHTTPHeaderField:#"email"];
[[APIClient sharedClient].requestSerializer setValue:password forHTTPHeaderField:#"password"];
[[APIClient sharedClient].requestSerializer setValue:#"poop" forHTTPHeaderField:#"apikey"];
return [[APIClient sharedClient] POST:#"/login" parameters:nil progress:nil success:^(NSURLSessionDataTask * __unused task, id JSON) {
NSLog(#"session.loginWithEmail.response:%#",JSON);
if([JSON objectForKey:#"user"]){
NSMutableDictionary *user=[[NSMutableDictionary alloc] initWithDictionary:[[JSON objectForKey:#"user"] copy]];
[user setObject:password forKey:#"password"];
[[Session sharedInstance] startSession:user];
if([[Session sharedInstance] isSessionActive]){
if([JSON objectForKey:#"req_onboarding"]){
NSLog(#"session.onboard!=nil");
[Session sharedInstance].requiredOnboarding=[JSON objectForKey:#"req_onboarding"];
}
if (block) {
NSLog(#"session.login.block.success");
block(nil, nil);
}
}else{
NSLog(#"Failed to set session");
}
}
} failure:^(NSURLSessionDataTask *__unused task, NSError *error) {
if (block) {
NSLog(#"Session.login.Fail");
block([NSArray array], error);
}
}];
}
I needed a sub-class-able singleton in order to be able to have a abstracted session manager that does most of the lifting,but can still be subclassed so that multiple sessions can co-exist and still have the power of being available throughout my app. Im building somewhat of a demo of all my apps which is why this functionality is important.
All was going well until I realized that my api methods that are hosted in the super session class were referencing the singleton itself to set the session, this is a problem bc sharedInstance is referenced like so:
+ (instancetype)sharedInstance
{
NSLog(#"[Master sharedInstance]");
id sharedInstance = nil;
#synchronized(self) {
NSLog(#"MS | synchronized(self)");
NSString *instanceClass = NSStringFromClass(self);
// Looking for existing instance
sharedInstance = [_sharedInstances objectForKey:instanceClass];
// If there's no instance – create one and add it to the dictionary
if (sharedInstance == nil) {
NSLog(#"MS | sharedInstance == nil");
sharedInstance = [[super allocWithZone:nil] init];
[_sharedInstances setObject:sharedInstance forKey:instanceClass];
NSLog(#"MS | SharedInstances:%#",_sharedInstances);
}
}
return sharedInstance;
}
When it was just the one Session singleton I could get away with doing this in class methods: [Session sharedInstance] isSessionActive]
but now, its essential that [______ sharedInstance] isSessionActive];
is a reference to the specific subclass calling the class method. Is it possible to retrieve reference the specific instance from within this class method shy of sending it as a param?
It looks like the aim is to distribute a singleton per subclass of the super (probably abstract) class. The sharedInstance code doesn't quite do that because this line:
sharedInstance = [[super allocWithZone:nil] init];
will create instances only of the superclass. I think you want instances of the subclasses so that you get access to the overridden data and behavior.
If I understand your aim correctly, then the fix is simple:
sharedInstance = [[self allocWithZone:nil] init]; // notice "self"
With this, when you send sharedInstance to ClassA, you'll get a (single) instance of ClassA. When you send it to ClassB, you'll get a (single) instance of ClassB. With change I suggest, those will really be instances of the subclasses A and B, not an instance of the class they both inherit from. If they've each overridden isSessionActive or any other superclass method, the caller will get the distinct, overridden implementation based on which class singleton they ask for.
Related
I want try to understand block capture logic and now I have question about it. I have MeRequest and NSNumber properties.
#property (nonatomic) MeRequest *request;
#property (nonatomic) NSNumber *number;
Then, in viewDidLoad i call request method
self.request = [[MeRequest alloc] init];
[self.request meInfoSuccessBlock:^(NSDictionary *response) {
} failureBlock:^(Error *error) {
self.number = #5;
}];
- (void)meInfoSuccessBlock:(RequestSuccessBlock)success failureBlock:(RequestFailureBlock)failure {
self.method = #"GET";
self.parameters = #{};
[self performWithCompletion:^(id responseObject) {
NSDictionary *response = (NSDictionary *)responseObject;
if (success) {
success(response);
}
} onFailure:^(Error *error) {
if (failure) {
failure(error);
}
}];
}
- (AFHTTPRequestOperation *)performWithCompletion:(void(^)(id responseObject))completion
onFailure:(void(^)(Error *error))failure {
NSURLRequest *request = [[NetworkManager sharedManager] requestWithMethod:self.method path:self.path parameters:self.parameters];
if (_operation) {
[_operation cancel];
}
_operation = [[NetworkManager sharedManager] HTTPRequestOperationWithRequest:request success:^(AFHTTPRequestOperation *operation, id responseObject) {
_operation = nil;
dispatch_semaphore_signal(_semaphore);
if (completion) {
completion(responseObject);
}
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
_operation = nil;
dispatch_semaphore_signal(_semaphore);
if (failure) {
failure(_error);
}
}];
[_operation start];
return _operation;
}
And in failureBlock I set number to property. When I leave this controller I see dealloc message in console, that controller has been dealloced.
- (void)dealloc {
NSLog(#"%s", __PRETTY_FUNCTION__);
}
Why controller deallocs? I don't use weak reference to self
To know definitively, you'd have to post the implementation of the MeRequest class.
Without knowing that, this is an educated guess.
The blocks passed into self.request via meInfoSuccessBlock:failureBlock: may be nil'd out when the transaction is complete. That is, it may be something like:
- (void)meInfoSuccessBlock:... sB failureBlock:... fB {
_sB = sB; // put ref in ivar
_fB = fB; // because this is probably broken up across methods
dispatch_async(_queue, ^{
.... think hard ...
if (success) _sB(...);
else _fB(...);
_sB = nil;
_fB = nil;
};
}
So, first, you aren't creating a direct cyclic reference, but -- maybe -- a cyclic reference of self -> request -> _sB -> self. And, secondly, by assigning _sB = nil after computation is done and the callback is made, the cycle is broken.
Or, in your case, you have strong references to the blocks that only survive the scope. I.e. kinda like this:
- (void)meInfoSuccessBlock:... sB failureBlock:... fB {
dispatch_async(_queue, ^{
.... think hard ...
if (success) sB(...);
else fB(...);
// when the block finishes execution, fB and sB will be released
};
// when execution gets to here, the block above is the only strong references to sB and fB
}
That is, while you have a retain cycle, one reference in that cycle is explicitly tied to the lifespan of the callback blocks and since those only survive until the callback is complete, they get destroyed and that destroys the cycle.
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.
In all of my iOS application I use this approach to respect MVC, I want to be sure that my implementation is correct and respects the best practices and the MVC design pattern :
Singleton of AFNetworking acting as API for network calls:
MyAPI.h :
#import "AFHTTPSessionManager.h"
#import "AFNetworking.h"
#interface MyAPI : AFHTTPSessionManager
+(MyAPI *)sharedInstance;
#end
MyAPI.m :
#pragma mark - Singleton
+(MyAPI*)sharedInstance
{
static MyAPI *sharedInstance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedInstance = [[MyAPI alloc] initWithBaseURL:[NSURL URLWithString:kROOT_URL]];
});
return sharedInstance;
}
Model User that uses the singleton to fecth the data of user (is that good as implementation ?):
User.h
#interface User : NSObject
#property (strong,nonatomic) NSString *userId;
#property (strong,nonatomic) NSString *email;
#property (strong,nonatomic) NSString *password;
-(id) initWithDictionary: (NSDictionary *) dictionay;
+(BOOL) isConnected;
+(void) disconnect;
+(NSString *) idOfConnectedUser;
+(User *) connectedUser;
+(void) loginWith : (NSString *) email andPassword :(NSString *) password complete:(void(^)(id result, NSError *error))block;
+(void) searchUsersFrom : (NSString *) countryCode withName :(NSString *) name andLevel:(NSString *) levelCode complete: (void(^)(id result, NSError *error)) block;
+(void) signup:(void(^)(id result, NSError *error)) block;
+(void) getUserFriends:(void(^)(id result, NSError *error)) block;
#end
User.m
[......]
+(void) loginWith : (NSString *) email andPassword :(NSString *) password complete: (void(^)(id result, NSError *error)) block
{
__block NSString * result ;
NSDictionary *params = #{#"email": email, #"password": password};
[[MyAPI sharedInstance] POST:#"auth/" parameters:params success:^(NSURLSessionDataTask *task, id responseObject)
{
if([responseObject objectForKey:#"id"])
{
[[NSUserDefaults standardUserDefaults] setObject:(NSDictionary*) responseObject forKey:USER_KEY];
[[NSUserDefaults standardUserDefaults] synchronize];
result = [responseObject objectForKey:#"id"];
}
else
{
result = nil ;
}
if (block) block(result, nil);
} failure:^(NSURLSessionDataTask *task, NSError *error)
{
if (block) block(nil, error);
}];
}
[.....]
LoginController.m :
-(void)loginButtonAction:(UIButton *)sender
{
[......]
[ User loginWith:text andPassword:text complete:^(id result, NSError *error)
{
if (result)
{
[APPDELEGATE start];
}
else
{
// ERROR
}
}];
}
So does my implementation respects the MCV and follows the best practices and how can I improve it if not ?
Singletons: You might want to avoid using singletons, it'll help you to improve your API design and make code more testable. Also, in case of User, imagine you will want to support changing user (logout/guest user/etc). With current approach, you will be limited to sending a NSNotification because everyone who uses connectedUser can not know that underlying reference has changed.
ActiveRecord:
What you did with your model User that is capable of performing networking is somewhat similar to active record approach which might not scale so well when you model becomes more complicated and the number of actions it can perform increases. Consider separating those into pure model and services that actually perform networking (or whatever else you will need in the future).
Model Serialisation:
Consider encapsulating model & network response serialisation logic into a separate class (e.g. LoginResponse that among other things points to a User) frameworks like Mantle make it much easier.
MVC: from my experience in iOS MVC might not be the most optimal approach for anything but simple apps. With MVC tendency is to put put all the logic into your ViewController making it very big and hard to maintain. Consider other patterns such as MVVM
All in all I understand that it is hard to learn all the new technologies at once, but you can definitely start by making sure each class performs one thing and one thing only: Model does not do networking or persisting to the disk, API client doesn't deserialise each response or saves data to NSUserDefaults, view controller doesn't do anything except for listening to user events (button taps etc). This alone would make your code much easier to reason about and to follow if a new developer would be introduced to your codebase.
Hope it helps!
I dont have anything to say about your MVC(Model–view–controller) correct?
I just want to add something that may be useful approach avoiding unwanted crashes..
First is under
[[MyAPI sharedInstance] POST:#"auth/" parameters:params success:^(NSURLSessionDataTask *task, id responseObject)
{
if([responseObject objectForKey:#"id"])
{
[[NSUserDefaults standardUserDefaults] setObject:(NSDictionary*) responseObject forKey:USER_KEY];
[[NSUserDefaults standardUserDefaults] synchronize];
result = [responseObject objectForKey:#"id"];
}
else
{
result = nil ;
}
}];
it is always possible for so many reason to mention that the reponseObject could be nil and therefore object dont have the key #"id" and will lead to error(crash in worst case). i have this code i dont know if this can be considered as best practice but here it is:
if ([responseObject isKindOfClass:[NSArray class]])
{
NSLog(#"Log: Response is of class NSArray");
}
else if ([responseObject isKindOfClass:[NSDictionary class]])
{
NSLog(#"Log: Response is of class NSDictionary");
}
else
{
NSLog(#"Log: Kind of class is not supported");
}
This example restricts other kind of class especially [NSNull class]
Second in line:
NSDictionary *params = #{#"email": email, #"password": password};
by checking email and password first before assigning to NSDictionary, setting nil to NSDictionary will cause crash.
Third is line:
if (block) block(result, nil);
block returns void from your implementation. Is this working? Sorry for asking i haven't tried if-statement with a block like this..
complete: (void(^)(id result, NSError *error)) block
void in this is the returning value of your block, or i'm wrong.. hmm..
if (block) only checks if the block block exist, so intead of checking it (which we are sure that is existing)..
maybe you want to check the result instead...
if (result != nil) block(result, nil); is the right statement
the reason behind is:
if (result != nil) // will return NONE nil value only
{
block(result, nil);
}
// else will not set things to the block
//or maybe just
block(result, nil); // which will allow the 'block(nil, nil);' and under your implementation
[ User loginWith:text andPassword:text complete:^(id result, NSError *error)
{
if (result)
{
[APPDELEGATE start];
}
else if (result == nil & error == nil)
{
// NO objectForKey #"id"
}
else
{
// ERROR
}
}];
while under failure:^(NSURLSessionDataTask *task, NSError *error) simply block(nil, error);
I am converting a project to an SDK. I need to convert several instance methods to class methods. I am getting a compiler warning about using "self". The warning is "Incompatible pointer types initializing Store* with an expression of Class. This Store class is a singleton sharedInstance.
I have method like this in my class Store:
+ (void) dispatchStoreSource {
__weak Store *ref = self; <--- issue is here
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSError *error = nil;
NSArray *things = [ref fetchThings:&error];
//dispatch back to main queue
if (![ref updateSource:source forUser:user error:&error]) {
dispatch_async(dispatch_get_main_queue(), ^{
result(nil, error);
});
} else {
dispatch_async(dispatch_get_main_queue(), ^{
result(source, nil);
});
}
});
}
What is the proper way to fix this? Should it I do this?
__weak Store *ref = [Store sharedInstance];
Your ref is pointer to an object of Store class. But self in your class method doesn't point to an allocated object of your class (= your singleton), it's your Class, IOW Store (not object, but Class). If you have implemented sharedInstance class method, like this ...
+ (instancetype)sharedInstance {
static Story *instance;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[self alloc] init];
});
return instance;
}
... just do this ref = [self sharedInstance];
Yes, You should go with __weak Store *ref = [Store sharedInstance];
Otherwise Let's use your original static reference of Store.
Example :
static Store = _store = nil;
__weak Store * ref = _store;
i am new to iOS programming, still learning.
EDIT: !!!!!! Everything in my code works. My question is about the delegation pattern i use,
if i am generating problems in the background that i have no idea of, or if there is a better way to handle my situation in AFNetworking...
I have created an API for my app by subclassing AFHTTPSessionManager.
My API creates a singleton and returns it and supplies public functions for various requests. And those functions create parameter lists, and make GET requests on the server like this:
- (void)getCharacterListForKeyID:(NSString *)keyID vCode:(NSString *)vCode sender:(id)delegate
{
NSMutableDictionary *parameters = [NSMutableDictionary dictionary];
parameters[#"keyID"] = keyID;
parameters[#"vCode"] = vCode;
[self GET:#"account/Characters.xml.aspx" parameters:parameters success:^(NSURLSessionDataTask *task, id responseObject) {
self.xmlWholeData = [NSMutableDictionary dictionary];
self.errorDictionary = [NSMutableDictionary dictionary];
NSXMLParser *XMLParser = (NSXMLParser *)responseObject;
[XMLParser setShouldProcessNamespaces:YES];
XMLParser.delegate = self;
[XMLParser parse];
if ([delegate respondsToSelector:#selector(EVEAPIHTTPClient:didHTTPRequestWithResult:)]) {
[delegate EVEAPIHTTPClient:self didHTTPRequestWithResult:self.xmlWholeData];
}
} failure:^(NSURLSessionDataTask *task, NSError *error) {
if ([delegate respondsToSelector:#selector(EVEAPIHTTPClient:didFailWithError:)]) {
[delegate EVEAPIHTTPClient:self didFailWithError:error];
}
}];
}
I was using a normal protocol/delegate method earlier. But once i make calls this API more than once like this: (IT WAS LIKE THIS:)
EVEAPIHTTPClient *client = [EVEAPIHTTPClient sharedEVEAPIHTTPClient];
client.delegate = self;
[client getCharacterListForKeyID:self.keyID vCode:self.vCode];
Previous call's delegate was being overwritten by next. So i changed to above style. Passing sender as an argument in the function:
EVEAPIHTTPClient *client = [EVEAPIHTTPClient sharedEVEAPIHTTPClient];
[client getCharacterListForKeyID:self.keyID vCode:self.vCode sender:self];
And i pass this sender to GET request's success and failure blocks.
What i wonder is : "Is this a good programming practice ?". Passing objects to blocks like this should be avoided if possible ? Is there any other more elegant way in AFHTTPSessionManager to handle this type of work (making same GET request over and over with different parameters and returning results to the respective request owners) more elegantly ?
Delegation pattern falters when it comes to simplicity and asynchronous request processing. You should be using blocks, here's an example
Your server class:
static NSString *const kNews = #"user_news/"; // somewhere above the #implementation
- (NSURLSessionDataTask *)newsWithPage:(NSNumber *)page
lastNewsID:(NSNumber *)lastNewsID
completion:(void (^)(NSString *errMsg, NSArray *news, NSNumber *nextPage))completionBlock {
return [self GET:kNews
parameters:#{#"page" : page,
#"news_id" : lastNewsID
}
success:^(NSURLSessionDataTask *task, id responseObject) {
NSArray *news = nil;
NSNumber *nextPage = nil;
NSString *errors = [self errors:responseObject[#"errors"]]; // process errors
if ([responseObject[#"status"] boolValue]) {
news = responseObject[#"news"];
nextPage = responseObject[#"next_page"];
[self assignToken];
}
completionBlock(errors, news, nextPage);
}
failure:^(NSURLSessionDataTask *task, NSError *error) {
NSString *errors = [self errors:error];
completionBlock(errors, nil, nil);
}];
}
The caller
- (void)dealloc {
[_task cancel]; // you don't want this task to execute if user suddenly removes your controller from the navigation controller's stack
}
- (void)requestNews {
typeof(self) __weak wself = self; // to avoid the retain cycle
self.task = [[GSGServer sharedInstance] newsWithPage:self.page
lastNewsID:self.lastNewsID
completion:^(NSString *errMsg, NSArray *news, NSNumber *nextPage) {
if (errMsg) {
[GSGAppDelegate alertQuick:errMsg]; // shortcut for posting UIAlertView, uses errMsg for message and "Error" as a title
return;
}
[wself.news addObjectsFromArray:news];
wself.lastNewsID = [wself.news firstObject][#"id"];
wself.page = nextPage;
[wself.tableView reloadData];
}];
}