I have recently set-up an in-app purchase mechanism in my app. During the purchase i would like to update an hud (i am using the mbprogresshud) according to two kinds of events: i start a purchase and i receive a validation of the purchase. The problem i am facing is that the hud is never updated with the one i want when the purchase is done (custom view):
When i click on the purchase button:
-(IBAction)buyButtonTapped:(id)sender {
self.hud = [[SCLProgressHUD alloc] initWithView:self.view];
[self.view addSubview:self.hud];
self.hud.labelText = #"Connecting...";
self.hud.minSize = CGSizeMake(100 , 100);
[self.hud show:YES];
...
}
When i receive a notification that the purchase was successful:
-(void)productPurchased:(NSNotification *)notification {
self.hud.customView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"checkmark_icon.png"]];
self.hud.mode = SCLProgressHUDModeCustomView;
self.hud.labelText = #"Thanks for your purchase!";
...
}
Setting the self.hud.customView property in the last method will trigger a [self setNeedsLayout]; and [self setNeedsDisplay] within the hud class but still i don't observe any change.
Any possible idea of what i am doing wrong here?
As mentioned on the mbprogress hud readme, update of the UI when tasks are performed on the main thread required a slight delay to take effect. What happen in my case is that the hud was a strong property of a popover controller that i dismiss immediately so i didn't get a chance to see the update happening. I now dismiss the controller in the completion block of:
-(void)showAnimated:(BOOL)animated
whileExecutingBlock:(dispatch_block_t)block completionBlock:(void
(^)())completion
And my code snippet look like like this for the dismiss:
[_hud showAnimated:YES whileExecutingBlock:^(void){
[self.successPurchaseSoundEffect play];
_hud.customView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"checkmark_icon.png"]];
_hud.mode = SCLProgressHUDModeCustomView;
_hud.labelText = #"Thanks!";
// We need to pause the background thread to let the music play and the hud be updated
sleep(1);}
completionBlock:^(void){
[self.delegate dismissPurchaseInfoController:self];
}];
Related
I am trying to display a message in the foreground by calling drawRect: or by adding a view that appears in foreground. The message must be displayed after pressing a button. I have noticed that the message is displayed only if the function that is called when button is pressed had exited.
- (IBAction)testBtnPress:(id)sender {
TestView *testView = [[TestView alloc] initWithFrame:self.view.frame];
[self.view addSubview:testView];
// processing to be done while the message is displayed
}
The drawRect: is called in the TestView class.
The rectangle with message is displayed after the processing to be done is finished, which is not convenient for my application.
Is there a way to display the message while the processing in running (the message will be "Processing... Please Wait").
Thanks.
The processing must either be done in a background thread, or performed in the next iteration of the runloop, to give your UI changes time to be handled.
You can do the processing in the next iteration of the runloop with something like this:
TestView *testView = ... ;
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
// processing to be done while the message is displayed
}];
This should work:
dispatch_async(dispatch_get_main_queue(), ^{
TestView *testView = [[TestView alloc] initWithFrame:self.view.frame];
[self.view addSubview:testView];
});
I have a little iPhone app that loads data from a web service. To make sure that nothing goes wrong while loading the data I create a semi-transparent view over the app and use CFRunloopRun() to wait until all the data is loaded in the background. This is the code for that:
self.connection = [[[NSURLConnection alloc] initWithRequest:request delegate:self] autorelease];
// Now show an animation
UIActivityIndicatorView *spinner = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge];
UIView *window = [[UIApplication sharedApplication] keyWindow];
UIView *shield = [[UIView alloc] initWithFrame:window.bounds];
shield.backgroundColor = [UIColor blackColor];
shield.alpha = 0.5f;
[window addSubview:shield];
spinner.center = shield.center;
[shield addSubview:spinner];
spinner.hidden = NO;
NSLog( #"JCL.callServerWithRequest(), spinner view: %#, shield view: %#, window: %#", spinner, shield, window );
[spinner startAnimating];
// Hand over to the Runnloop to wait
CFRunLoopRun();
[spinner stopAnimating];
[spinner removeFromSuperview];
[spinner release];
[shield removeFromSuperview];
[shield release];
This works fine except that any clicks on a button somewhere is played after the loading so if the users clicks on the download button twice he will do the download twice as well.
Any idea how to consume the UI events before the shield is removed.
Thanks - Andy
Try it without messing with runloops. I suspect that the UI events are coming in to the normal loop on the window but not being processed until your custom loop returns, at which point the "shield" view is no longer there to catch them. If you put the shield in place and then let the main runloop handle things, the shield should catch them all as normal.
Thanks to Anomie I finally tried out to go without the CFRunLoopRun() and it is quite difficult because the execution is split into two parts: - the Invocation and the Return of the Result through a callback. But then I shot myself in the proverbial foot because I tried to block the returning thread to slow down the execution which did not work because that was executed again in the main thread.
Eventually I did slow down the Web Service and then everything worked as expected.
In my iPhone app, I want to add activity indicator on top of a searchbar.
When it is searching it should display activity indicator.
I have added the activity indicator in XIB and created its outlet.
I am making it hide when the searching finishes, but Activity Indicator does not display.
Problem
I figured out that search function(say A)(where I animate the activity indicator) in turn calls another function(say B) so the main thread is being used in executing the function B. But for activity indicator to animate we require the main thread.
So I tried calling function B using performSelectorInBackGround:withObject method. Now when I click search the activity indicator is shown but the functionality of function B does not execute.
What can be a work-around for this?
There is not quite enough in your question to go on, but to start debugging, I would do the following.
Verify that the activity variably is really wired to the UIActivityIndicator you are creating in IB. (I would set a breakpoint on the setHidden: lines and make sure the variable is not null. Or throw an NSAssert(activity,#"Whoops! actity is null"); in there.)
If the variable is indeed set, I would start checking that it is in the right place in the view hierarchy. (I'd try doing a [self.view addSubview:activity] and see that it appears. You might have to replace it somewhere else.)
You might also want to try having it on by default in IB, until you have everything figured out.
Good Luck. Hope this helps.
Save yourself the hassle of creating a custom activity indicator and use the standard one that's available for you already - in the top bar. Also, IMO users tend to expect that one to spin when something is happening.
UIApplication* app = [UIApplication sharedApplication];
app.networkActivityIndicatorVisible = YES;
Obviously, set it to NO when your activity is over.
First of all, make sure you have #synthesize activity at the top of your .m file. Then in the viewDidLoad method, type activity.hidesWhenStopped = TRUE;. Next, in the method that is called when the search starts, type [activity startAnimating]; and [activity stopAnimating]; in the method when the searching stops.
try this:
set hidesWhenStopped = NO, so that is displayed all the time and then hide and show it manually. But the View should be set in IB to hidden first.
- (void)startActivityView {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc]init];
activity_view.hidden = NO;
[pool drain];
}
- (void)stopActivityView {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc]init];
activity_view.hidden = YES;
[pool drain];
}
- (void)doSomething {
[self performSelectorInBackground:#selector(startActivityView) withObject:nil];
// do some time consuming work
[self performSelectorInBackground:#selector(stopActivityView) withObject:nil];
}
Perhaps you have a view in front of your activity indicator? What if you always bring it to the front....
loadView = [[UIActivityIndicatorView alloc]
initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge];
loadView.frame = CGRectMake(0.0, 0.0, 40.0, 40.0);
loadView.center = window.center;
loadView.opaque = NO;
[window addSubview: loadView];
[window bringSubviewToFront:loadView];
[loadView startAnimating];
I suggest that you use DSActivityView for showing your activity indicator. The source code can be found at Dejal blog.
Showing, and hiding, the activity view is a simple line of code.
[DSActivityView activityViewForView:self.view];
start animating the activity indicator and with a delay of 0.1 or 0.2 just call the other method u want.... i tried and it is working for me....
I have got the solution and it is as follows.
I just wrote the below line in Search button click event.
[NSThread detachNewThreadSelector:#selector(threadStartAnimating:) toTarget:self withObject:nil];
And defined the function threadStartAnimating: as follows:
-(void)threadStartAnimating:(id)data
{
[activityIndicator setHidden:NO];
[activityIndicator startAnimating];
}
I'm trying to create an activity indicator in iPhone app. The problem is that I cannot get it to appear before the actual task i want it to diplay during is already done. Is there something funky about the order in which the iPhone does stuff?
Here is my problematic code (in my app delegate):
-(BOOL)showProgressView: (NSString *) message {
self.progress = [[UIView alloc] initWithFrame:window.frame];
UIImageView *img = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"squircle.png"]];
[img setAlpha:0.5];
[img setFrame:CGRectMake(94, 173, 133, 133)];
UIActivityIndicatorView *spinner = [[UIActivityIndicatorView alloc] initWithFrame:CGRectMake(51.5, 51.5, 30, 30)];
spinner.activityIndicatorViewStyle = UIActivityIndicatorViewStyleWhiteLarge;
[img addSubview:spinner];
[self.progress addSubview:img];
[spinner startAnimating];
[img release];
[spinner release];
[window addSubview:self.progress];
return YES;
}
I then call this code like this:
if ([appDelegate showProgressView:#"Loading..:"])
{
//My actual code loads data and stuff here but that is not important
//drawCtrl is a UIViewController subclass that is instantiated here
UINavigationController *navController = [appDelegate navigationController];
[navController pushViewController:drawCtrl animated:YES];
[drawCtrl release];
}
The problem is that my activity indicator does not appear until the new view controller is pushed onto the navController's stack. Can I control this in some way?
Thanks in advance!
-Mats
You need to call your loading method periodically via a timer. UI changes require a trip through the event loop to be seen. I have done this usually in a timer I set up in the AppDelegate, it calls the "methodThatTakesSomeTime" every n seconds, the "methodThatTakesSomeTime" method does some work for a specified time slice and then exits which is usually about the same as the timer trigger time. This gives the event loop time to refresh the UI and also give your method time to do its stuff.
Takes some fiddling with keeping the state of the "methodThatTakesSomeTime" so it can continue on its way, but that is what it takes.
I am totally stumped, here's the situation:
My app uses the Core Location framework to get the current location of the user and then pings my server at TrailBehind for interesting places nearby and displays them as a list. No problems.
To conserve batteries, I turn off the GPS service after I get my data from the server. If the user moves around while using the app and wants a new list he clicks "Refresh" on the navigation controller and the CLLocation service is again activated, a new batch of data is retrieved from the server and the table is redrawn.
While the app is grabbing data from my server I load a loading screen with a spinning globe that says "Loading, please wait" and I hide the navigation bar so they don't hit "back".
So, the initial data grab from the server goes flawlessly.
The FIRST time I hit refresh all the code executes to get a new location, ping the server again for a new list of data and updates the cells. However, instead of loading the table view as it should it restores the navigation controller bar for the table view but still shows my loading view in the main window. This is only true on the device, everything works totally fine in the simulator.
The SECOND time I hit refresh the function works normally.
The THIRD time I hit refresh it fails as above.
The FOURTH time I hit refresh it works normally.
The FIFTH time I hit refresh it fails as above.
etc etc, even refreshes succeed and odd refreshes fail. I stepped over all my code line by line and everything seems to be executing normally. I actually continued stepping over the core instructions and after a huge amount of clicking "step over" I found that the table view DOES actually display on the screen at some point in CFRunLoopRunSpecific, but I then clicked "continue" and my loading view took over the screen.
I am absolutely baffled. Please help!! Many thanks in advance for your insight.
Video of the strange behavior:
Relevant Code:
RootViewControllerMethods (This is the base view for this TableView project)
- (void)viewDidLoad {
//Start the Current Location controller as soon as the program starts. The Controller calls delegate methods
//that will update the list and refresh
[MyCLController sharedInstance].delegate = self;
[[MyCLController sharedInstance].locationManager startUpdatingLocation];
lv = [[LoadingViewController alloc] initWithNibName:#"Loading" bundle:nil];
[self.navigationController pushViewController:lv animated:YES];
[super viewDidLoad];
}
- (void)updateClicked {
//When the location is successfully updated the UpdateCells method will stop the CL manager from updating, so when we want to update the location
//all we have to do is start it up again. I hope.
[[MyCLController sharedInstance].locationManager startUpdatingLocation];
[self.navigationController pushViewController:lv animated:YES];
//LV is a class object which is of type UIViewController and contains my spinning globe/loading view.
}
-(void)updateCells {
//When the Core Location controller has updated its location it calls this metod. The method sends a request for a JSON dictionary
//to trailbehind and stores the response in the class variable jsonArray. reloadData is then called which causes the table to
//re-initialize the table with the new data in jsonArray and display it on the screen.
[[MyCLController sharedInstance].locationManager stopUpdatingLocation];
if(self.navigationController.visibleViewController != self) {
self.urlString = [NSString stringWithFormat:#"http://www.trailbehind.com/iphone/nodes/%#/%#/2/10",self.lat,self.lon];
NSURL *jsonURL = [NSURL URLWithString:self.urlString];
NSString *jsonData = [[NSString alloc] initWithContentsOfURL:jsonURL];
NSLog(#"JsonData = %# \n", jsonURL);
self.jsonArray = [jsonData JSONValue];
[self.tableView reloadData];
[self.navigationController popToRootViewControllerAnimated:YES];
[jsonData release];
}
}
CLController Methods: Basically just sends all the data straight back to the RootViewController
// Called when the location is updated
- (void)locationManager:(CLLocationManager *)manager
didUpdateToLocation:(CLLocation *)newLocation
fromLocation:(CLLocation *)oldLocation
{
NSLog(#"New Location: %# \n", newLocation);
NSLog(#"Old Location: %# \n", oldLocation);
#synchronized(self) {
NSNumber *lat = [[[NSNumber alloc] init] autorelease];
NSNumber *lon = [[[NSNumber alloc] init] autorelease];
lat = [NSNumber numberWithFloat:newLocation.coordinate.latitude];
lon = [NSNumber numberWithFloat:newLocation.coordinate.longitude];
[self.delegate noteLat:lat];
[self.delegate noteLon:lon];
[self.delegate noteNewLocation:newLocation];
[self.delegate updateCells];
}
}
The first thought is that you may not want to send startUpdatingLocation to the CLLocationManager until after you've pushed your loading view. Often the first -locationManager:didUpdateToLocation:fromLocation: message will appear instantly with cached GPS data. This only matters if you're acting on every message and not filtering the GPS data as shown in your sample code here. However, this would not cause the situation you've described - it would cause the loading screen to get stuck.
I've experienced similarly weird behavior like this in a different situation where I was trying to pop to the root view controller when switching to a different tab and the call wasn't being made in the correct place. I believe the popToRootViewController was being called twice for me. My suspicion is that your loading view is either being pushed twice or popped twice.
I recommend implementing -viewWillAppear:, -viewDidAppear:, -viewWillDisappear: and -viewDidDisappear: with minimal logging in your LoadingViewController.
- (void)viewWillAppear:(BOOL)animated {
NSLog(#"[%# viewWillAppear:%d]", [self class], animated);
[super viewWillAppear:animated];
}
- (void)viewDidAppear:(BOOL)animated {
NSLog(#"[%# viewDidAppear:%d]", [self class], animated);
[super viewDidAppear:animated];
}
- (void)viewWillDisappear:(BOOL)animated {
NSLog(#"[%# viewWillDisappear:%d]", [self class], animated);
[super viewWillDisappear:animated];
}
- (void)viewDidDisappear:(BOOL)animated {
NSLog(#"[%# viewDidDisappear:%d]", [self class], animated);
[super viewDidDisappear:animated];
}
Then, run a test on your device to see if they are always being sent to your view controller and how often. You might add some logging to -updateClicked to reveal double-taps.
Another thought, while your #synchronized block is a good idea, it will only hold off other threads from executing those statements until the first thread exits the block. I suggest moving the -stopUpdatingLocation message to be the first statement inside that #synchronized block. That way, once you decide to act on some new GPS data you immediately tell CLLocationManager to stop sending new data.
Can you try and debug your application to see where the control goes when calling updateCells? Doesn't seem to be anything apparently wrong with the app.
Make sure that there are no memory warnings while you are in the LoadingViewController class. If there is a memory warning and your RootViewController's view is being released, then the viewDidLoad will be called again when you do a pop to RootViewController.
Keep breakpoints in viewDidLoad and updateCells. Are you sure you are not calling LoadingViewController anywhere else?
So, I never did get this to work. I observe this behavior on the device only every time I call popViewController programatically instead of allowing the default back button on the navigation controller to do the popping.
My workaround was to build a custom loading view, and flip the screen to that view every time there would be a delay due to accessing the internet. My method takes a boolean variable of yes or no - yes switches to the loading screen and no switches back to the normal view. Here's the code:
- (void)switchViewsToLoading:(BOOL)loading {
// Start the Animation Block
CGContextRef context = UIGraphicsGetCurrentContext();
[UIView beginAnimations:nil context:context];
[UIView setAnimationTransition: UIViewAnimationTransitionFlipFromLeft forView:self.tableView cache:YES];
[UIView setAnimationCurve:UIViewAnimationCurveEaseInOut];
[UIView setAnimationDuration:.75];
// Animations
if(loading) {
if (lv == nil) { lv = [[LoadingViewController alloc] initWithNibName:#"Loading" bundle:nil]; }
[self.view addSubview:lv.view];
[self.view sendSubviewToBack:self.tableView];
self.title = #"TrailBehind";
}
else {
[lv.view removeFromSuperview];
}
// Commit Animation Block
[UIView commitAnimations];
//It looks kind of dumb to animate the nav bar buttons, so set those here
if(loading) {
self.navigationItem.rightBarButtonItem = nil;
self.navigationItem.leftBarButtonItem = nil;
self.title = #"TrailBehind";
}
else {
UIBarButtonItem *feedback = [[UIBarButtonItem alloc] initWithTitle:#"Feedback" style:UIBarButtonItemStylePlain target:self action:#selector(feedbackClicked)];
self.navigationItem.rightBarButtonItem = feedback;
UIBarButtonItem *update = [[UIBarButtonItem alloc] initWithTitle:#"Move Me" style:UIBarButtonItemStylePlain target:self action:#selector(updateClicked)];
self.navigationItem.leftBarButtonItem = update;
[feedback release];
[update release];
}
}
Looking at your original code, I suspect this block very much:
- (void)viewDidLoad {
...
lv = [[LoadingViewController alloc] initWithNibName:#"Loading" bundle:nil];
[self.navigationController pushViewController:lv animated:YES];
[super viewDidLoad];
}
viewDidLoad is called every time the NIB is loaded, which can happen multiple times, especially if you run low on memory (something that seems likely given your remark that it only happens on device). I recommend that you implement -didReciveMemoryWarning, and after calling super, at the very least print a log so you can see whether it's happening to you.
The thing that bothers me about the code above is that you're almost certainly leaking lv, meaning that there may be an increasing number of LoadingViewControllers running around. You say it's a class variable. Do you really mean it's an instance variable? ivars should always use accessors (self.lv or [self lv] rather than lv). Do not directly assign to them; you will almost always do it wrong (as you are likely dong here).
I came across this while searching for the exact same issue, so while I'm sure you've already solved your problem by now, I figured I'd post my solution in case someone else runs across it...
This error seems to be caused when you assign two IBActions to the same UIButton in interface builder. It turned out that the button I used to push the view controller onto the stack was assigned to two IBActions, and each one was pushing a different controller onto the navigationController's stack (although you'll only end up seeing one of them - perhaps the last one to be called). So anyway, pressing the back button on the topmost view doesn't really dismiss it (or maybe it's dismissing the 2nd, unseen controller), and you have to press twice to get back.
Anyway, check your buttons and be sure they're only assigned to a single IBAction. That fixed it for me.