I am creating a update method for in house enterprise apps. What I am trying to create is a class that I can quickly put in a app and it will check with the server if it needs to be updated. This is what I have so far, it checks correctly but its returning NO before it finishes the method.
In my checkUpdate.h
#interface checkForUpdate : NSObject
+ (BOOL)updateCheck;
#end
In my checkUpdate.m
#import "checkForUpdate.h"
#implementation checkForUpdate
BOOL needsUpdate
NSDictionary *versionDict;
#define kBgQueue dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
+ (BOOL)updateCheck {
NSString *urlStringVersion = [[NSString alloc] initWithFormat:#"http://URL/app_info?app=app"];
NSURL *urlVersion = [NSURL URLWithString:urlStringVersion];
dispatch_async(kBgQueue, ^{
NSData* data =[NSData dataWithContentsOfURL:urlVersion];
if (data){
NSError* error;
NSArray* json = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:&error];
if (json != nil && [json count] != 0) {
versionDict = [json objectAtIndex:0];
CGFloat serverVersion = [[versionDict valueForKey:#"version"]floatValue];
CGFloat appVersion = [[[[NSBundle mainBundle] infoDictionary] objectForKey:(NSString*)kCFBundleVersionKey] floatValue];
NSLog(#"Server Version %f",serverVersion);
NSLog(#"App Version %f",appVersion);
if ([versionDict count] != 0){
if (serverVersion > appVersion){
[[UIApplication sharedApplication] setApplicationIconBadgeNumber:1];
needsUpdate = YES;
}else{
[[UIApplication sharedApplication] setApplicationIconBadgeNumber:0];
needsUpdate = NO;
}
}
}
}
});
return needsUpdate;
}
#end
I call it like this
NSLog(#"Needs Update %#",[checkForUpdate checkForUpdateWithResponse] ? #"Yes":#"No");
This is my log
Needs Update No
Server Version 2.000000
App Version 1.000000
I'm not sure why it's returning NO before it even checks. I need it to be a asynchronous because the server that the app will check with is behind our firewall. So if the person is outside the firewall the app needs to continue when is can't reach the server. I'm I headed in the right direction, or is there a better way?
You are asynchronously checking for an update but expecting an immediate response by virtue of your method's design. You can re-engineer your method to something like the example below to notify a handler whenever the operation is complete:
Note: Unchecked and untested for errors; however, the lesson to be gleaned from the example is to use a callback of sorts:
UpdateChecker Class
typedef void (^onComplete)(BOOL requiresUpdate);
#interface UpdateChecker : NSObject
-(void)checkForUpdates:(onComplete)completionHandler;
#end
#implementation UpdateChecker
-(void)checkForUpdates:(onComplete)completionHandler
{
NSString *urlStringVersion = [[NSString alloc] initWithFormat:#"http://URL/app_info?app=app"];
NSURL *urlVersion = [NSURL URLWithString:urlStringVersion];
dispatch_block_t executionBlock =
^{
/*
Your update checking script here
(Use the same logic you are currently using to retrieve the data using the url)
*/
NSData* data = [NSData dataWithContentsOfURL:urlVersion];
BOOL requiresUpdate = NO;
if (data)
{
...
...
...
requiresUpdate = ...; //<-whatever your outcome
}
//Then when completed, notify the handler (this is our callback)
//Note: I typically call the handler on the main thread, but is not required.
//Suit to taste.
dispatch_async(dispatch_get_main_queue(),
^{
if (completionHandler!=NULL)
completionHandler(requiresUpdate);
});
};
dispatch_async(kBgQueue, executionBlock);
}
#end
This is what it would look when you use UpdateChecker to check for updates throughout your app
UpdateChecker *checker = [UpdateChecker alloc] init];
[checker checkForUpdates:^(BOOL requiresUpdate)
{
if (requiresUpdate)
{
//Do something if your app requires update
[[UIApplication sharedApplication] setApplicationIconBadgeNumber:1];
}
else
[[UIApplication sharedApplication] setApplicationIconBadgeNumber:0];
}];
Since dispatch_async is non-blocking, your method returns before your update information has returned (does the dispatch and continues). As needsUpdate defaults to NO, that's what you'll see. You can see this in your log timing - the "Needs Update No" shows up before the server and app versions.
You need some sort of callback (a delegate method or second dispatch_async for example) to ensure you get the correct result, or you need to block. I recommend looking into NSURLConnection, and sendAsynchronousRequest:queue:completionHandler: - it will execute the completion handler on completion, where you can have whatever code you need for handling the update.
Related
I have this code:
- (NSString *) login {
datos=#"";
NSString __block *variable;
NSString *sqlQueryExisteUsuario;
sqlQueryExisteUsuario = [[NSString alloc] initWithFormat:#"SELECT COUNT(*) FROM tableName WHERE field='value' AND field='value'"];
SQLClient* client = [SQLClient sharedInstance];
client.delegate = self;
[client connect:#"serverName" username:#"username" password:#"password" database:#"database" completion:^(BOOL success) {
[client execute:sqlQueryExisteUsuario completion:^(NSArray* results) {
variable = [self processLogin:results];
NSLog(#"In: %#",variable);
[client disconnect];
}];
}];
NSLog(#"Out: %#",variable);
return nil;
}
- (NSString *)processLogin:(NSArray*)data
{
existeArray = [NSMutableArray array];
for (NSArray* table in data)
for (NSDictionary* row in table)
for (NSString* column in row)
[existeArray addObject:row[column]];
NSString *existe=existeArray[0];
if([existe isEqualToString:#"1"])
{
datos=#"yes";
}else{
datos=#"no";
}
return datos;
}
In the first call to NSLog, which begins with In, the value shows. In the second call, which begins with Out, the value doesn't show. Why?
Your connect is async method, so NSLog... line will be executed earlier than completion block. So, you have to use blocks also:
- (NSString *) loginWithCompletion:(void(^)(NSString *result))handler
{
datos=#"";
NSString *sqlQueryExisteUsuario;
sqlQueryExisteUsuario = [[NSString alloc] initWithFormat:#"SELECT COUNT(*) FROM tableName WHERE field='value' AND field='value'"];
SQLClient* client = [SQLClient sharedInstance];
client.delegate = self;
[client connect:#"serverName" username:#"username" password:#"password" database:#"database" completion:^(BOOL success) {
if (success) {
[client execute:sqlQueryExisteUsuario completion:^(NSArray* results) {
NSString *variable = [self processLogin:results];
NSLog(#"In: %#",variable);
[client disconnect];
if (handler) {
handler (variable);
}
}];
} else {
//TODO: handle this
if (handler) {
handler (nil);
}
}
}];
}
Usage:
- (void)ff
{
[self loginWithCompletion:^(NSString *variable) {
//Do something with variable
}];
}
The problem is that your variable is being set inside a completion block: the variable variable (not a great name, BTW!) is set inside not only one but two blocks - that's the "completion" part of your code – both of which are best thought of a bit(!) like a miniature anonymous function: in this case, they get run when the system is ready for it, not straight away.
iOS will start execution of your connect code, then jump ahead to NSLog(#"Out: %#",variable), then execute the completion block of connect, which in turn starts more code (execute), which has its own completion block, which finally gets executed. As #rmaddy says in a comment below, the technical name for this is asynchronous code: the bit inside your block does not get executed immediately, which allows the system to continue doing other things while waiting for your task to complete.
So the running order will be:
1) You declare variable.
2) Your connection code starts.
3) variable gets printed out.
4) The connection completes.
5) Your execute code starts.
6) Your execute code completes.
7) variable gets set to the final value.
Im fetching data from database through a php-API with this code:
- (void) viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
[self loadDataWithSpinner];
[self reloadAllData];
}
- (void) loadDataWithSpinner {
if (!self.spinner) [self setupSpinnerView];
self.sessions = nil;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void){
self.userId = [[NSUserDefaults standardUserDefaults] integerForKey:#"CurrentUserId"] ;
self.sessions = [self getAllSessionsForUserId:self.userId];
dispatch_async(dispatch_get_main_queue(), ^(void){
if (self.sessions) {
self.spinnerBgView.hidden = YES;
[self setupChartsAndCountingLabelsWithData];
}
});
});
}
- (NSArray *) getAllSessionsForUserId: (int) userId {
NSData *dataURL = nil;
NSString *strURL = [NSString stringWithFormat:#"http://webapiurl.com/be.php?queryType=fetchAllBasicSessions&user_id=%i", userId];
dataURL = [NSData dataWithContentsOfURL:[NSURL URLWithString:strURL]];
NSError *error = nil;
if (dataURL) {
NSArray *sessions = [NSJSONSerialization JSONObjectWithData:dataURL options:kNilOptions error:&error];
return sessions;
} else {
return Nil;
}
}
Is there something wrong with the code? I'm getting the correct data when testing in a web-browser with the same database call. But in the app it sometimes updates straight away, as it should, and sometimes it takes up to five minutes for the data to update. Even though i remove the app from the phone/simulator, the data sometime hasn't been update when opening the app again.
I would really appreciate some help.
I finally found the answer. Its nothing wrong with my code, as I thought. The problem lies on the server side. And, as they say in this thread:
NSURLConnection is returning old data
It seems to be badly configured server-side or proxy. Which makes the cache act all wierd. I tried another server and everything worked just fine!
Hope this helps someone with the same issue, cause it sure wasted me ALOT of time.
I'm a bit confuse of how to take advantage of the new iOS 7 NSURLSession background transfers features and AFNetworking (versions 2 and 3).
I saw the WWDC 705 - What’s New in Foundation Networking session, and they demonstrated background download that continues after the app terminated or even crashes.
This is done using the new API application:handleEventsForBackgroundURLSession:completionHandler: and the fact that the session's delegate will eventually get the callbacks and can complete its task.
So I'm wondering how to use it with AFNetworking (if possible) to continue downloading in background.
The problem is, AFNetworking conveniently uses block based API to do all the requests, but if the app terminated or crashes those block are also gone. So how can I complete the task?
Or maybe I'm missing something here...
Let me explain what I mean:
For example my app is a photo messaging app, lets say that I have a PhotoMessage object that represent one message and this object has properties like
state - describe the state of the photo download.
resourcePath - the path to the final downloaded photo file.
So when I get a new message from the server, I create a new PhotoMessage object, and start downloading its photo resource.
PhotoMessage *newPhotoMsg = [[PhotoMessage alloc] initWithInfoFromServer:info];
newPhotoMsg.state = kStateDownloading;
self.photoDownloadTask = [[BGSessionManager sharedManager] downloadTaskWithRequest:request progress:nil destination:^NSURL *(NSURL *targetPath, NSURLResponse *response) {
NSURL *filePath = // some file url
return filePath;
} completionHandler:^(NSURLResponse *response, NSURL *filePath, NSError *error) {
if (!error) {
// update the PhotoMessage Object
newPhotoMsg.state = kStateDownloadFinished;
newPhotoMsg.resourcePath = filePath;
}
}];
[self.photoDownloadTask resume];
As you can see, I use the completion block to update that PhotoMessage object according to the response I get.
How can I accomplish that with a background transfer? This completion block won't be called and as a result, I can't update the newPhotoMsg.
A couple of thoughts:
You have to make sure you do the necessary coding outlined in the Handling iOS Background Activity section of the URL Loading System Programming Guide says:
If you are using NSURLSession in iOS, your app is automatically relaunched when a download completes. Your app’s application:handleEventsForBackgroundURLSession:completionHandler: app delegate method is responsible for recreating the appropriate session, storing a completion handler, and calling that handler when the session calls your session delegate’s URLSessionDidFinishEventsForBackgroundURLSession: method.
That guide shows some examples of what you can do. Frankly, I think the code samples discussed in the latter part of the WWDC 2013 video What’s New in Foundation Networking are even more clear.
The basic implementation of AFURLSessionManager will work in conjunction with background sessions if the app is merely suspended (you'll see your blocks called when the network tasks are done, assuming you've done the above). But as you guessed, any task-specific block parameters that are passed to the AFURLSessionManager method where you create the NSURLSessionTask for uploads and downloads are lost "if the app terminated or crashes."
For background uploads, this is an annoyance (as your task-level informational progress and completion blocks you specified when creating the task will not get called). But if you employ the session-level renditions (e.g. setTaskDidCompleteBlock and setTaskDidSendBodyDataBlock), that will get called properly (assuming you always set these blocks when you re-instantiate the session manager).
As it turns out, this issue of losing the blocks is actually more problematic for background downloads, but the solution there is very similar (do not use task-based block parameters, but rather use session-based blocks, such as setDownloadTaskDidFinishDownloadingBlock).
An alternative, you could stick with default (non-background) NSURLSession, but make sure your app requests a little time to finish the upload if the user leaves the app while the task is in progress. For example, before you create your NSURLSessionTask, you can create a UIBackgroundTaskIdentifier:
UIBackgroundTaskIdentifier __block taskId = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^(void) {
// handle timeout gracefully if you can
[[UIApplication sharedApplication] endBackgroundTask:taskId];
taskId = UIBackgroundTaskInvalid;
}];
But make sure that the completion block of the network task correctly informs iOS that it is complete:
if (taskId != UIBackgroundTaskInvalid) {
[[UIApplication sharedApplication] endBackgroundTask:taskId];
taskId = UIBackgroundTaskInvalid;
}
This is not as powerful as a background NSURLSession (e.g., you have a limited amount of time available), but in some cases this can be useful.
Update:
I thought I'd add a practical example of how to do background downloads using AFNetworking.
First define your background manager.
//
// BackgroundSessionManager.h
//
// Created by Robert Ryan on 10/11/14.
// Copyright (c) 2014 Robert Ryan. All rights reserved.
//
#import "AFHTTPSessionManager.h"
#interface BackgroundSessionManager : AFHTTPSessionManager
+ (instancetype)sharedManager;
#property (nonatomic, copy) void (^savedCompletionHandler)(void);
#end
and
//
// BackgroundSessionManager.m
//
// Created by Robert Ryan on 10/11/14.
// Copyright (c) 2014 Robert Ryan. All rights reserved.
//
#import "BackgroundSessionManager.h"
static NSString * const kBackgroundSessionIdentifier = #"com.domain.backgroundsession";
#implementation BackgroundSessionManager
+ (instancetype)sharedManager {
static id sharedMyManager = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedMyManager = [[self alloc] init];
});
return sharedMyManager;
}
- (instancetype)init {
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:kBackgroundSessionIdentifier];
self = [super initWithSessionConfiguration:configuration];
if (self) {
[self configureDownloadFinished]; // when download done, save file
[self configureBackgroundSessionFinished]; // when entire background session done, call completion handler
[self configureAuthentication]; // my server uses authentication, so let's handle that; if you don't use authentication challenges, you can remove this
}
return self;
}
- (void)configureDownloadFinished {
// just save the downloaded file to documents folder using filename from URL
[self setDownloadTaskDidFinishDownloadingBlock:^NSURL *(NSURLSession *session, NSURLSessionDownloadTask *downloadTask, NSURL *location) {
if ([downloadTask.response isKindOfClass:[NSHTTPURLResponse class]]) {
NSInteger statusCode = [(NSHTTPURLResponse *)downloadTask.response statusCode];
if (statusCode != 200) {
// handle error here, e.g.
NSLog(#"%# failed (statusCode = %ld)", [downloadTask.originalRequest.URL lastPathComponent], statusCode);
return nil;
}
}
NSString *filename = [downloadTask.originalRequest.URL lastPathComponent];
NSString *documentsPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0];
NSString *path = [documentsPath stringByAppendingPathComponent:filename];
return [NSURL fileURLWithPath:path];
}];
[self setTaskDidCompleteBlock:^(NSURLSession *session, NSURLSessionTask *task, NSError *error) {
if (error) {
// handle error here, e.g.,
NSLog(#"%#: %#", [task.originalRequest.URL lastPathComponent], error);
}
}];
}
- (void)configureBackgroundSessionFinished {
typeof(self) __weak weakSelf = self;
[self setDidFinishEventsForBackgroundURLSessionBlock:^(NSURLSession *session) {
if (weakSelf.savedCompletionHandler) {
weakSelf.savedCompletionHandler();
weakSelf.savedCompletionHandler = nil;
}
}];
}
- (void)configureAuthentication {
NSURLCredential *myCredential = [NSURLCredential credentialWithUser:#"userid" password:#"password" persistence:NSURLCredentialPersistenceForSession];
[self setTaskDidReceiveAuthenticationChallengeBlock:^NSURLSessionAuthChallengeDisposition(NSURLSession *session, NSURLSessionTask *task, NSURLAuthenticationChallenge *challenge, NSURLCredential *__autoreleasing *credential) {
if (challenge.previousFailureCount == 0) {
*credential = myCredential;
return NSURLSessionAuthChallengeUseCredential;
} else {
return NSURLSessionAuthChallengePerformDefaultHandling;
}
}];
}
#end
Make sure app delegate saves completion handler (instantiating the background session as necessary):
- (void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)())completionHandler {
NSAssert([[BackgroundSessionManager sharedManager].session.configuration.identifier isEqualToString:identifier], #"Identifiers didn't match");
[BackgroundSessionManager sharedManager].savedCompletionHandler = completionHandler;
}
Then start your downloads:
for (NSString *filename in filenames) {
NSURL *url = [baseURL URLByAppendingPathComponent:filename];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
[[[BackgroundSessionManager sharedManager] downloadTaskWithRequest:request progress:nil destination:nil completionHandler:nil] resume];
}
Note, I don't supply any of those task related blocks, because those aren't reliable with background sessions. (Background downloads proceed even after the app is terminated and these blocks have long disappeared.) One must rely upon the session-level, easily recreated setDownloadTaskDidFinishDownloadingBlock only.
Clearly this is a simple example (only one background session object; just saving files to the docs folder using last component of URL as the filename; etc.), but hopefully it illustrates the pattern.
It shouldn't make any difference whether or not the callbacks are blocks or not. When you instantiate an AFURLSessionManager, make sure to instantiate it with NSURLSessionConfiguration backgroundSessionConfiguration:. Also, make sure to call the manager's setDidFinishEventsForBackgroundURLSessionBlock with your callback block - this is where you should write the code typically defined in NSURLSessionDelegate's method:
URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session. This code should invoke your app delegate's background download completion handler.
One word of advice regarding background download tasks - even when running in the foreground, their timeouts are ignored, meaning you could get "stuck" on a download that's not responding. This is not documented anywhere and drove me crazy for some time. The first suspect was AFNetworking but even after calling NSURLSession directly, the behaviour remained the same.
Good luck!
AFURLSessionManager
AFURLSessionManager creates and manages an NSURLSession object based on a specified NSURLSessionConfiguration object, which conforms to <NSURLSessionTaskDelegate>, <NSURLSessionDataDelegate>, <NSURLSessionDownloadDelegate>, and <NSURLSessionDelegate>.
link to documentation here documentation
NSString *pictureUrl = [[[oneUserDict objectForKey:#"picture"]objectForKey:#"data"]objectForKey:#"url"];
[[AppEngine sharedEngine]imageAtURL:[NSURL URLWithString:pictureUrl] onCompletion:^(UIImage *fetchedImage, NSURL *url, BOOL isInCache)
{
int index = [usersArray indexOfObject:oneUserDict];
NSString *loadName = [NSString stringWithFormat:#"%d of %d",index,[usersArray count]];
NSLog(#"%i",usersArray.count);
int temp=[usersArray count]-10;
if (index!=temp)
{
[[LoadingIndicator currentIndicator]displayActivity:loadName];
NSLog(#"inside loading indicator");
}
else
{
[[LoadingIndicator currentIndicator]displayCompleted:#"Done"];
NSLog(#"finally done");
}
aPerson.image = UIImagePNGRepresentation(fetchedImage);
[appDelegate.managedObjectContext save:nil];
}];
AppEngine is the subclass of MKNetworkEngine which uses a method called imageAtURL:onCompletion:
what I am currently doing is retrieving all the images from a particular url and and storing them in aPerson.image,basically the above code is in a FOR loop(i.e the for the count of users).
Issues
The above code which is in the completion block never gets executed,i dont know why but i have put a breakpoint inside the block but still the compiler wont run the statements inside the completion block.
Api imageAtURL:onCompletion: is deprecated. Use imageAtURL:completionHandler:errorHandler: instead. Also MKNetworkKit provides for UIImageView+MKNetworkKitAdditions category which provides simple API for image download like setImageFromURL: placeHolderImage:
Cheers!
Amar.
I get this warning in Xcode
warning: Attempting to create USE_BLOCK_IN_FRAME variable with block
that isn't in the frame.
Xcode redirect me to my NSStream
_naturStream = [[NSInputStream alloc] initWithData:natur];
It is random when it does this error, and my application crashes when it is triggered. Anyone tried similar problem ?
thanks
EDIT
in the appDelegate.h
#property (nonatomic, strong) NSInputStream *naturStream;
In the appDelegate.m:
NSData *natur = [NSData dataWithContentsOfURL:[NSURL URLWithString:_locString]];
_naturStream = [[NSInputStream alloc] initWithData:natur];
[_naturStream open];
if (_naturStream) {
NSError *parseError = nil;
id jsonObject = [NSJSONSerialization JSONObjectWithStream:_naturStream options:NSJSONReadingAllowFragments error:&parseError];
if ([jsonObject respondsToSelector:#selector(objectForKey:)]) {
for (NSDictionary *natur in [jsonObject objectForKey:#"results"]) {
_poi = [[POI alloc]init];
[_poi setTitle:[natur objectForKey:#"title"]];
[_poi setLat:[[natur objectForKey:#"lat"]floatValue]];
[_poi setLon:[[natur objectForKey:#"lng"]floatValue]];
[_poi setDistance:[natur objectForKey:#"distance"]];
[_poi setWebUrl:[natur objectForKey:#"webpage"]];
[_naturArray addObject:_poi];
}
}
}
else {
NSLog(#"Failed to open stream.");
}
[_naturStream close];
}
I realized that i forgot [_naturStream close] i don't know if it has solved the problem or not ?
EDIT
Another thing,.... I use a Thread for fetching the JSON data:
dispatch_queue_t jsonParsingQueue = dispatch_queue_create("jsonParsingQueue", NULL);
// execute a task on that queue asynchronously
dispatch_async(jsonParsingQueue, ^{
[self parseJSON];
dispatch_async(dispatch_get_main_queue(), ^{
[_kortvisning updateAnno];
[visListe updateList];
});
});
// release the dispatch queue
dispatch_release(jsonParsingQueue);
Sounds like you're using ARC - if _naturStream is an instance variable for an objective C class, you might need to pull it out and add a __block reference so that ARC knows the scope correctly - but I'm guessing because I don't see how the block is used with the NSInputStream (if you post that part we might know). A good bit is here: http://nachbaur.com/blog/using-gcd-and-blocks-effectively
-- edit --
Ok, now that you posted the rest, I bet it has to do with the _kortvisning and visListe variables. I think you want to pull those out right after you create your queue something like
__block KortVisning *localKortVisning = _kortvisning;
__block NSMutableArray *localVisListe = visListe;
Then access those directly from your final completion handler you're sending back to the main queue.