I have a problem with my network activity indicator in that sometimes it will continue to be displayed when it should not be.
I wrote my own manager for it and swapped it out for one that uses an NSAssert statement like this...
- (void)setNetworkActivityIndicatorVisible:(BOOL)setVisible {
static NSInteger NumberOfCallsToSetVisible = 0;
if (setVisible)
NumberOfCallsToSetVisible++;
else
NumberOfCallsToSetVisible--;
// The assertion helps to find programmer errors in activity indicator management.
// Since a negative NumberOfCallsToSetVisible is not a fatal error,
// it should probably be removed from production code.
NSAssert(NumberOfCallsToSetVisible >= 0, #"Network Activity Indicator was asked to hide more often than shown");
// Display the indicator as long as our static counter is > 0.
[[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:(NumberOfCallsToSetVisible > 0)];
}
I found it on SO and it has immediately pointed out that something is going wrong with my use of this function.
All of my network activity is exclusively run through a single NSOperationQueue which is managed by a singleton class. Every operation is a subclass of NSOperation (actually a subclass of a TemplateOperation which is a subclass of NSOperation).
Anyway, all the downloads and uploads are working fine and I'm doing them all like this...
- (void)sendRequest:(NSURLRequest *)request
{
NSError *error = nil;
NSURLResponse *response = nil;
[[NetworkManager sharedInstance] setNetworkActivityIndicatorVisible:YES];
NSData *data = [NSURLConnection sendSynchronousRequest:request
returningResponse:&response
error:&error];
[[NetworkManager sharedInstance] setNetworkActivityIndicatorVisible:NO];
// other stuff...
[self processData:data];
}
The important lines are immediately before and after I send the NSURLConnection synchronously.
Immediately before I send the request I set the network activity indicator to visible (using my manager class) and then immediately after I set it back to invisible.
Except the NSAssert has pointed out that somewhere this is not happening properly.
Could it be that running this function from multiple threads could be causing an issue? How could I solve this?
Integer increment or decrement is not thread-safe (as far as I know), so if two threads call your method "simultaneously", the count might not get updated properly.
One solution would be to add some synchronization directive (such as #synchronized)
to your method. Or you use the atomic increment/decrement functions:
#include <libkern/OSAtomic.h>
- (void)setNetworkActivityIndicatorVisible:(BOOL)setVisible {
static volatile int32_t NumberOfCallsToSetVisible = 0;
int32_t newValue = OSAtomicAdd32((setVisible ? +1 : -1), &NumberOfCallsToSetVisible);
NSAssert(newValue >= 0, #"Network Activity Indicator was asked to hide more often than shown");
[[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:(newValue > 0)];
}
Related
I have created a UIButton and on click event, I am showing an image in the web view. Also, I am refreshing the image in every 30 sec. But when I click on button multiple times, refresh method get called multiple time as well.
I want it to work like, It saves last click time and refreshes as per that time instead of multiple times.
What can I do for it?
I tried to kill all previous thread instead of the current thread but that's not working.
Please help if anyone already know the answer.
Below is my image refresh code:
- (void)refreshBanner:(id)obj {
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
if (![SNRunTimeConfiguration sharedInstance].isInternetConnected) {
[self removeBannerAdWithAdState:kADViewStateNotConnectedToInternet];
return;
}
if ([UIApplication sharedApplication].applicationState == UIApplicationStateBackground) {
self.bannerPaused = YES;
return;
}
self.adView.hidden = YES;
UIViewController *topController = [UIApplication sharedApplication].keyWindow.rootViewController;
topController = [SNADBannerView topViewControllerWithRootViewController:topController];
if ([self checkInViewHierarchy:self parentView:topController.view]) {
// NSLog(#"Visible View Is: %#", self.adId);
SNADMeta *meta = [[SNADDataBaseManager singletonInstance] adToShowWithBanner:YES excludeTyrooAd:YES audio:NO zoneId:self.adSoptZoneId fixedView:NO condition:nil contextualKeyword:nil onlyFromAJ:NO];
SNADAdLocationType type = SNADAdLocationTypeHeader;
if (self.bannerType == SmallViewTypeFooter) {
type = SNADAdLocationTypeFooter;
}
if (self.isFromCustomEvent) {
type = SNADAdLocationTypeAdMobBanner;
}
NSString *message = meta ? nil : kSNADOppMissReason_NoAdToShow;
[SNRunTimeConfiguration fireOpportunityForAdLocation:type zoneId:self.adSoptZoneId reason:message];
NSLog(#"******************* Opportuninty fired for refresh banner ***************************");
if (meta) {
self.meta = meta;
[self updateContentForWebAd:nil];
[self updateStatsForAd];
//fireImpression
[SNADBannerView fireImpression:self.meta];
if ([meta.adSource isEqualToString:kSNADParameter_APC]) {
self.sdkMediation = [[SdkMediation alloc] init];
[self.sdkMediation fireTrackingAdType:self.meta.type isFill:YES];
}
// Ad Height Delegate.
if ([self.meta.displayType isEqualToString:kSNADDisplayType_web]) {
self.adHeightDelegateCalled = YES;
NSInteger height = self.meta.height.integerValue;
self.bannerCH.constant = height;
if ([self.callBackDelegate respondsToSelector:#selector(adWillPresentWithHeight:adId:adType:)]) {
[self.callBackDelegate adWillPresentWithHeight:height adId:self.adId adType:SeventynineAdTypeMainStream];
}
}
} else {
[self removeBannerAdWithAdState:kADViewStateNoAdToShow];
if ([meta.adSource isEqualToString:kSNADParameter_APC]) {
[self.sdkMediation fireTrackingAdType:self.meta.type isFill:NO];
}
return;
}
} else {
// NSLog(#"View Which Is Not Visible Now: %#", self.adId);
}
SNAdConfiguration *configuration = [SNAdConfiguration sharedInstance];
[self.timer invalidate];
self.timer = [NSTimer scheduledTimerWithTimeInterval:configuration.autoRefRate target:self selector:#selector(refreshBanner:) userInfo:nil repeats:NO];
}];
}
Use GCD, and not NSOperationQueue.
Then you step away from your immediate task. You do lots and lots of complicated things inside refreshBanner. And you will do more complicated things to make it work when the user taps multiple times.
Think about what exactly you need. Abstract the "refresh automatically, and when the button is clicked, but not too often" into a class. Then you create a class that takes a dispatch_block_t as an action, where a caller can trigger a refresh anytime they want, and the class takes care of doing it not too often. Then you create an instance of the class, set all the needed refresh actions as its action block, refreshBanner just triggers a refresh, and that class takes care of the details.
You do that once. When you've done it, you actually learned stuff and are a better programmer than before, and you can reuse it everywhere in your application, and in new applications that are coming.
NSOperationQueue have cancelAllOperations method. But for the main queue it's not a good decision to use this method, cause main queue is shared between different application components. You can accidentally cancel some iOS/other library operation together with your own.
So you can create NSOperation instances and store them in an array. Then you can call cancel for all scheduled operations by iterating trough this array, and it will only affect your operations.
Note that block operations doesn't support cancellation. You will need to create your own NSOperation subclass, extract code from your execution block into that subclass main method. Also, you'll need to add [self isCancelled] checks that will abort your logic execution at some points.
I forgot to mention that currently your execution block is fully performed on the main queue. So, you'll need to move any heavy-lifting to background thread if you want to cancel your operation in the middle of processing from main thread.
I need to add that I agree with #gnasher729 - this doesn't look like an optimal solution for the problem.
I have resolved the issue.
Multiple threads created because a new view is created every time I call the API to display image. So now I am removing views if any available before displaying image, then only last object remains and refresh is called as per last called time.
Every View has it's own object that's why multiple threads has created.
By removing views my issue has been resolved.
Thanks everyone for replying.
I have a memory leak that seems to be coming from a retain cycle. The memory allocation size is increasing every time this code runs:
- (void)nextPhoto {
self.photoIndex++;
if (self.photoIndex >= [self.photos count]) {
self.photoIndex = 0;
}
__weak Photo *photo = [self.photos objectAtIndex:self.photoIndex];
[[SDWebImageManager sharedManager] downloadImageWithURL:[NSURL URLWithString:photo.thumbnailURLString] options:SDWebImageRetryFailed progress:nil
completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
}];
}
The code is looping on a 2 second timer:
self.timer = [NSTimer scheduledTimerWithTimeInterval:2 target:self selector:#selector(nextPhoto) userInfo:nil repeats:YES];
The total memory use increases without bounds until I get a memory overuse event.
Why is this code causing a retain cycle? Is there a special way I need to handle self in this situation?
self.photos is an NSMutableArray
self.photoIndex is an NSInteger
SDWebImageManager is a well maintained library: https://github.com/rs/SDWebImage and I use it in numerous other locations with no issues
I don't see any problem involving a retain cycle here, even if you use self in the completion block. the block owner is SDWebImageManager so no problems here. a retain cycle could occur if you store your block in a property of your viewController, cause it then would own a block that retains it... It's not what is happening here imho.
Now your problem, i presume, comes from the UIImage. I depends of what you do in the block of course but if your storing the images then, yes every 2 seconds a new one is created and then it will fail eventually. You should keep a cache of images that has already been downloaded and try to download them only if needed... Add a NSDictionary with url as key and UIImage as value for example, this way you will only download your images once.
Ok I should have slept on this one... The function is actually working exactly as it should and it was self.photos that was increasing without bounds. Putting a limit on the size of that array fixed the "leak".
I'm having a problem where I'm unable to update UI when performing synchronous downloads. I would expect that using synchronous APIs would ensure that code executes in order (which it doesn't seem to be doing), which is really confusing me.
The following code is in a UICollectionView's didSelectItemAtIndexPath and is not wrapped in any asynchronous block or anything.
Any ideas on what I can do to be able to update the UI (most importantly a progress indicator) as these tasks occur? I think that the way it is currently laid out should work, but for some reason it's not able to update until the code has all 'executed'.
if ([internetReachable isReachable]) {
//does not become visible until after
self.circleProgress.alpha = 1.0;
//lots of downloading and saving with NSData dataWithContentsOfURL followed by this:
for (int i = 1; i < pages.count; i++) {
NSString *number;
if (i < 10) {
number = [NSString stringWithFormat:#"00%d", i];
}
else if (i < 100) {
number = [NSString stringWithFormat:#"0%d", i];
}
else {
number = [NSString stringWithFormat:#"%d", i];
}
NSURL *imageURL = [NSURL URLWithString:[NSString stringWithFormat:#"http://books.hardbound.co/%#/%#-%#.png", slug, slug, number]];
NSData *imageData = [NSData dataWithContentsOfURL:imageURL];
[df setObject:imageData forKey:[NSString stringWithFormat:#"%#-%#", slug, number]];
CGFloat progress = ((CGFloat)i / pages.count);
//only runs for the last iteration, rather than calling the method to update the progress indicator each iteration and allowing it to update before going back to the next iteration as I would expect
[self updateProgressBarWithAmount:[NSNumber numberWithFloat:progress]];
NSLog(#"progress after: %f", self.circleProgress.progress);
}
}
UI can only be executed on the main thread. Since the main thread is busy doing the downloading, it can't update the UI. It's almost never a good idea to perform any long running operations on the main thread. You should make the download asynchronous, and update the UI on the main thread.
The loop in the code you posted will only be executed after lots of downloading and saving with NSData dataWithContentsOfURL is performed, all the while the application will be unresponsive, and that's very poor UX. Take a look at this question for a much better implementation of a progress bar.
I am not by any means qualified to explain what exactly happens during each render loop and why updateProgress doesn't actually let a screen render occur before you block the main thread again, but I am able to provide a solution.
After you update the progress of the progress view, you want the changes to get rendered "right now". This means you have to tell the current run loop to run one iteration, and then return to you so you can do another long running task.
[[NSRunLoop currentRunLoop] runMode: NSDefaultRunLoopMode beforeDate: [NSDate date]];
Call that whoever you want the progress view to update, and it will do a screen render and then return to you.
I got this from this answer
However, you really should be doing this asynchronously.
(Apologies for any typos, as this is being typed on my phone)
I am making an app that shows an animated UIImageView as a custom way of indicating that the app is busy. I'm using an NSOperationQueue for file uploads, and I'd like the UIImageView to be shown when there is something in the queue. When every operation in the queue completes, I want to remove the UIImageView.
I thought that this is something really easy to do, but I've been stuck now for the past hour. Showing the UIImageView is really easy, but I can't seem to remove it. It's probably something really simple that I'm just overlooking. Here's my code. Thank you! :)
- (void)viewDidLoad {
//set up the uiimageview
self.spinnerView = [[UIImageView alloc] initWithFrame:CGRectMake([[UIScreen mainScreen] bounds].size.width-44,0,44,44)];
self.spinnerView.animationImages = [NSArray arrayWithObjects:
[UIImage imageNamed:#"0.gif"],
[UIImage imageNamed:#"1.gif"],
[UIImage imageNamed:#"2.gif"],
[UIImage imageNamed:#"3.gif"],
[UIImage imageNamed:#"4.gif"], nil];
self.spinnerView.animationDuration = 0.5f;
self.spinnerView.tag = 998;
self.spinnerView.animationRepeatCount = 0;
[self.view addSubview: self.spinnerView];
//set up the queue
self.uploadQueue = [[NSOperationQueue alloc] init];
[self.uploadQueue setMaxConcurrentOperationCount:1];
//set up observer for the queue
[self.uploadQueue addObserver:self forKeyPath:#"operationCount" options:NSKeyValueObservingOptionNew context:NULL];
}
- (void)newUpload:(NSData*)data {
[self.spinnerView startAnimating];
//....
//request is a NSURLRequest that's set up in this method
[NSURLConnection sendAsynchronousRequest:request queue:self.uploadQueue completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
}];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object
change:(NSDictionary *)change context:(void *)context
{
if (object == self.uploadQueue && [keyPath isEqualToString:#"operationCount"]) {
if (self.uploadQueue.operationCount == 0) {
[self.spinnerView stopAnimating];
}
}
else {
[super observeValueForKeyPath:keyPath ofObject:object
change:change context:context];
}
Am I doing this correctly? Is there a better way to do it? I've been stuck here for a while and am starting to think that perhaps it's not the UIImageView that's messing up, but rather the way that I'm adding NSURLRequests to the NSOperationQueue.
Thanks again!
Why don't you try https://github.com/samvermette/SVProgressHUD or https://github.com/jdg/MBProgressHUD ? They were made exactly for that purpose (showing modal loading window while doing some asynchronous job). And they are both easy to use and easy customizable for your images and many other options.
The documentation for sendAsynchronousRequest:queue:completionHandler: says that an operation is only added to the specified operation queue after the asynchronous URL request has completed. This operation is just a completion block.
So I do't think you are really adding adding operations in the way you intend to your queue. The URL requests will be running on their own threads outside the queue, only the completion blocks are put on the queue. If you haven't specified anything in the completion block itself then perhaps it is not even added to the queue at all?
Either way I don't think you are adding 5 URL operations to the queue, which then execute one after the other with operationCount == 5, 4, 3, 2, 1, 0. You are more likely firing 5 simultaneous URL requests with their completion blocks being added to the queue in an indeterminate sequence after their URL requests happen to finish.
To do what I think you intend to do, you could:
Write a "concurrent" NSOperation subclass that contains and
manages an NSURLConnection and NSURLRequest etc.
Use AFNetworking.
Continue with sendAsynchronousRequest:queue:completionHandler: but use the
completion handler of one operation to start the next request, and
the completion handler of the final request to stop the spinner.
You could just use the main queue here as the only work being done
in the queue is starting the next operation or stoping the spinner. The actual work of the URL Request is done on it's own thread anyway.
Writing your own concurrent NSOperation is a bit tricky, I wrote one myself, but I probably should have just used AFNetworking. Option 3 is probably the quickest if it meets your needs.
Add this to your .h file: UIImageView *spinnerView; In your .m file, you would want something like this in your -(void)newUpload:
if (code that says file uploads are done) {
spinnerView.hidden = YES;
}
I am quite new to iOS development and I'm facing a multithreading issue.
I'm using KTPhotobrowser with SDWebImage to create a photo and video gallery.
I have to load some external data on each picture, and I don't want to affect the smoothness of the gallery's scroll view.
So, I'm trying to do that using NSOperation and NSOperationQueue, but I'm not sure I'm doing right.
What I want is to stop the loading process if the user doesn't stay on the picture and keep scrolling.
My current code:
//setCurrentIndex is called when the scrollView is scrolled
- (void)setCurrentIndex:(NSInteger)newIndex {
[loadingQueue_cancelAllOperations];
currentIndex_ = newIndex;
[self loadPhoto:currentIndex_];
NSInvocationOperation *InvocationOp = [[NSInvocationOperation alloc] initWithTarget:self selector:#selector(loadInfosAtIndex:) object:newIndex];
[loadingQueue_ addOperation:InvocationOp];
[InvocationOp release];
[selfloadPhoto:currentIndex_ + 1];
[selfloadPhoto:currentIndex_ - 1];
[selfunloadPhoto:currentIndex_ + 2];
[selfunloadPhoto:currentIndex_ - 2];
}
-(void) loadInfosAtIndex:(NSInteger)index {
if (index < 0 || index >= photoCount_) {
return;
}
KTPhotoView* photoView = [photoViews_ objectAtIndex:index];
if([photoView infosAlreadyLoaded]){
NSLog(#"%d Already Loaded", index);
return;
}
//Get the extra information by calling a web service
photoView.infosAlreadyLoaded = YES;
}
I don't think this is the proper way to do this... has anyone got any advice?
Instead of relying on a scheduling-based cancel, which can leave you in an uncertain state, have a cancelling instance variable that has atomic access (either via an atomic property or a BOOL ivar with a mutex).
Then, instead of [loadingQueue_cancelAllOperations], you simply set the canceling flag to YES and check it in loadInfosAtIndex periodically. This is essentially polling for the cancel, and if the code is involved, it can be a pain. But it has the advantage of letting you be able to handle the cancel gracefully by reading the flag. As a part of handling, you can set an isRunning flag (also needs to be atomic/mutexed) to NO and exit the thread by returning.
In the main thread, after setting the cancelling flag to YES, you can wait till the isRunning flag is NO before opening up a new thread.
[loadingQueue_ cancelAllOperations], simply doesn't cancel the operations immediately, it only cancels the operations in the queue that has not yet started executing.
If the operation in the queue has already started then it is removed only after the operation is completed. More about cancelAllOperations
I think you might want to use GCD async for that where you might remove the currently executing block.