I'm examining some code written by someone else, as part of a new job I've been hired for. One of the bugs confronting me is that, on certain pages, when the MBProgressHUD is displayed for "Logging In" or "Connecting," it gets a chunk taken out of the middle. For example:
Obviously what I'm looking for is something a little more like this:
It only seems to happen when the app returns from the background (i.e. not when the app is first booting up, when we also use MBProgressHUD but it works perfectly), and only on certain pages. The box loads correctly, and then about half a turn of the Activity Indicator later, that hole appears. It then stays like that until the box disappears.
I'd add some code to look at, but to be perfectly honest, I don't know where to start. I can't think of anything that could take a chunk out of the middle like that, and as you can see from the transparency of the second picture, there doesn't seem to be a box of that shape/size behind the Activity Indicator that could be accidentally turning green.
I've never used MBProgressHUD myself before, and I've never encountered a graphical bug of this nature. Does anyone know what is going on, or failing that, can anyone give me some leads to investigate regarding what could be causing this behavior?
EDIT:
Below is the code used to add the Activity Indicator to the HUD (from within the MBProgressHUD object):
// Update to indeterminate indicator
[self.indicator removeFromSuperview];
self.indicator = nil;
if (IOSVersion >= 8.0 && (DeviceScreenSize().height >= 1136.0 || DeviceScreenSize().width >= 1136.0)) {
self.indicator = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge];
} else {
self.indicator = MB_AUTORELEASE([[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge]);
}
[(UIActivityIndicatorView *)indicator startAnimating];
[self addSubview:indicator];
This is part of a larger updateIndicators method, but the salient points are there: the indicator is removed (in case there was another there before), then it is re-added and animated. Comment out either of the startAnimating or addSubview lines, and the HUD appears without the Activity Indicator, but the problem never occurs.
That sounds to me like the animation of the Activity Indicator is somehow causing the missing piece of the underlying view. But why would that be? Has anyone heard of that kind of thing before?
EDIT 2:
As far as I can tell, the problem only happens on one ViewController in the whole app, and yet that ViewController never references MBProgressHUD or Activity Indicators of any kind. And all of that functionality is in the AppDelegate method applicationDidBecomeActive:, as below:
MBProgressHUD* hud = [[MBProgressHUD alloc] initWithWindow:self.window];
[self.window addSubview:hud];
hud.labelText = LocalizedString(#"Logging in...");
[hud showAnimated:YES whileExecutingBlock:^{
User* U = self.SelectedUser;
if (!isEmpty(U)) {
if ([U networkLogin]) {
[self setSelectedUser:U];
if ([U Disabled] != disabledTypesNone) {
[Flurry logEvent:#"Login Failed" withParameters:#{#"Name": U.DisplayName,
#"DeviceID": [#([Device sharedDevice].DeviceID) stringValue],
#"Disabled": [#([U Disabled]) stringValue]}];
ret = NO;
} else {
[Flurry logEvent:#"Login" withParameters:#{#"Name": U.DisplayName,
#"DeviceID": [#([Device sharedDevice].DeviceID) stringValue]}];
[Flurry setUserID:[NSString stringWithFormat:#"%# - %#", U.EmpID, U.DisplayName]];
}
} else {
ret = NO;
}
}
} completionBlock:^{
[hud removeFromSuperview];
if (!ret) {
[[NSNotificationCenter defaultCenter] postNotificationName:kDisplayLogin object:nil];
} else {
[[NSNotificationCenter defaultCenter] postNotificationName:kLoggedIn object:nil];
}
}];
Since it's happening in AppDelegate, it ought to be the same throughout the app, correct? And yet, when that same code is called (from AppDelegate, as before) while the app is on a different ViewController, it works with no problems.
What might make the behavior of that method different between different ViewControllers?
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'm making a synchronize function that syncs local Core Data with the server. I want to make the synchronizations happen in the background without disrupting user interaction. When I receive the response (whether success or failure) the app should display a message somewhere on the screen to notify the user about the outcome.
UIAlertController is not a good choice because it will block user action.
Currently I'm using SVProgressHUD:
__weak StampCollectiblesMainViewController *weakSelf = self;
if ([[AppDelegate sharedAppDelegate] hasInternetConnectionWarnIfNoConnection:YES]) {
[_activityIndicator startAnimating];
[Stamp API_getStampsOnCompletion:^(BOOL success, NSError *error) {
if (error) {
[_activityIndicator stopAnimating];
[SVProgressHUD setDefaultMaskType:SVProgressHUDMaskTypeClear];
[SVProgressHUD setAnimationDuration:0.5];
[SVProgressHUD showErrorWithStatus:#"error syncronize with server"];
}
else {
[_activityIndicator stopAnimating];
[featuredImageView setImageWithURL:[NSURL URLWithString:[Stamp featuredStamp].coverImage] usingActivityIndicatorStyle:UIActivityIndicatorViewStyleGray];
[yearDropDownList setValues:[Stamp yearsDropDownValues]];
[yearDropDownList selectRow:0 animated:NO];
[weakSelf yearDropDownListSelected];
[SVProgressHUD dismiss];
}
}];
}
Is there a modification I can make so the user can still interact with the app? I just want to show the message without taking up too much space. Any help is much appreciated. Thanks.
Looks like the easiest thing will be to use SVProgressHUDMaskTypeNone.
Also check out this issue.
Sorry but you gonna have to build your own custom view.
In fact it's not that difficult. What I would do is simply add a small view on the top of the screen with your custom message and a close button (to allow user to hide quickly the message). This is usually done by adding this new view to the current window, so that it will be on the top of every view and won't block the UI (except the part hidden by that view :) )
In our iPgone & iPad app we use push segue transitions between different ui contollers, most of them extend UICollectionViewController. In each controller we load data from our internal API. Loading is done viewWillAppear or viewDidLoad.
Now, the thing is, that this API call sometime can take a second or two, or even three... well, lot's of stuff there, let's assume we can't change it. But, we can change the user experience and at least add the "loading" circle indicator. The thing is, what I can't understand by means of correct concept, while transition from A to B, the "load" is done at B, while page A still presented.
So, question is "how do I show indicator on page A, while loading controller for page B?"
Thanks all,
Uri.
Common approach in this case is to load data in destination view controller NOT in main thread. You can show indicator while loading data in background thread and then remove it.
Here is sample of code from my project solving the same problem:
- (void) viewDidLoad {
...
// add indicator
self.spinner = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray];
self.spinner.hidesWhenStopped = YES;
self.spinner.center = self.view.center;
[self.view addSubview:self.spinner];
...
// fetch news
[self.spinner startAnimating];
__weak typeof(self) weakSelf = self
[[BitrixApiClient sharedInstance] getLatestNewsWithCompletionBlock:^(NSArray *newsArray, NSUInteger maxPageCount, NSUInteger currentPageNumber, NSError *error) {
if (!error) {
weakSelf.newsArray = newsArray;
weakSelf.currentPageNumber = currentPageNumber;
[weakSelf.newsTableView reloadData];
}
// stop spinning
[weakSelf.spinner stopAnimating];
}];
}
I have an app that makes web service calls to obtain data. I want to add an activity indicator that is visible when the app is fetching web service data. I have looked into other posts, and though I believe I am doing as the posts recommend, my indicator does not render on the screen. The object that makes the web service call is stateGauges. Here is my code:
- (void)viewDidLoad
{
[super viewDidLoad];
UIActivityIndicatorView *activityStatus = [[UIActivityIndicatorView alloc] initWithFrame:CGRectMake(120, 230, 50, 50)];
activityStatus.center = self.view.center;
[self.view addSubview:activityStatus];
[activityStatus bringSubviewToFront:self.view];
[UIApplication sharedApplication].networkActivityIndicatorVisible = TRUE;
[activityStatus startAnimating];
stateGauges = [[GaugeList alloc] initWithStateIdentifier:stateIdentifier andType:nil];
[activityStatus stopAnimating];
}
Any suggestions? Thanks! V
Your problem is that your animation start is blocked by whatever you're doing in your GuagesList initializer.
When you tell the activity indicator to start animating, it doesn't immediately render to the screen but rather flags the view as needing an update on the next turn of the run loop. Your initializer then blocks the thread until its done, you call stopAnimating, and then the thread has a chance to update the indicator. By which point its already set to not animate.
The best solution is to perform your initializer on another thread using GCD. And be sure to switch back to the foreground thread before calling stopAnimating.
The usual pattern is do something like:
[activityStatus startAnimating];
// enqueue it
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
stateGauges = [[GaugeList alloc] initWithStateIdentifier:stateIdentifier andType:nil];
// now switch back to main thread
dispatch_async(dispatch_get_main_queue(), ^{
[activityStatus stopAnimating];
});
});
You'll want to verify the code as I had to type this from memory on a Windows machine.
take out
[activityStatus bringSubviewToFront:self.view];
because according to the docs bringSubviewToFront:
Moves the specified subview so that it appears on top of its siblings.
which isn't what you want. (another answer suggested you do [self.view bringSubviewToFront:activityStatus] instead.. that's fine, but generally this call is redundant, b/c
[self.view addSubview:activityStatus] adds the activityStatus to the end of the views in the self.view subviews array anyways)
if that still don't work.. basically put a break point right after you start animating, then type this into the console:
[[activityStatus superview] recursiveDescription]
recursiveDescription will give you a UI tree graph and basically tell you exactly where the activityIndicator view is.. you may have made an incorrect assumption about something.
Change
[activityStatus bringSubviewToFront:self.view];
To
[self.view bringSubviewToFront:activityStatus];
Reasons for Rejection: The activity indicator spins indefinetely and the user can't access the content
The same situation,Second time be rejected because of used MBProgressHUD.
Who can tell me Uploaded to appstore app would be any different? I done various tests, such a problem did not appear in the local.
-----------------------------in my controller-----------------------------------
- (void)downloadList
{
if (isLoading) {
return;
}
isLoading = YES;
//do something......
//show the progressbar based on MBProgressHUD
[[MyDelegate getAppDelegate] showProgressBarForTarget:self whileExecuting:#selector(showProgressBarForLoading)];
}
}
- (void)showProgressBarForLoading
{
while (isLoading) {
//i++;
continue;
}
}
- (void)downloadListDidReceive:(XGooConnection*)sender obj:(NSObject*)obj
{
//do something......
isLoading = NO;
}
-----------------------------in my AppDelegate-------------------------------
- (void)showProgressBarForTarget:(id)target whileExecuting:(SEL)theSelector
{
UIViewController *controller = [splitViewController.viewControllers objectAtIndex:0];
HUD = [[MBProgressHUD alloc] initWithView:controller.view];
[controller.view addSubview:HUD];
HUD.delegate = self;
// Show the HUD while the provided method executes in a new thread
[HUD showWhileExecuting:theSelector onTarget:target withObject:nil animated:YES];
}
-----------------------------Reasons for Rejection detail-------------------------------------
The most recent version of your app has been rejected........
Reasons for Rejection:
The steps to reproduce are:
Launch the app
Select the Menu button at the top left corner
Select a menu item
The activity indicator spins indefinetely and the user can't access the content
First off, the reason for this rejection is likely improper usage of MBProgressHUD, not MBprogressHUD itself.
If this only occurs during app store testing, try running the app in Release configuration. There also might be networking conditions there, that you haven't anticipated. Perhaps this only occurs when there is a network error (airplane mode?). Are you setting isLoading = NO when a network error occurs?
FWIW, there is a much better way to show / hide the HUD for asynchronous requests. Pooling a flag in a while loop like this is extremely inefficient. Look at the NSURLConnection example in the MBProgressHUD demo app.