I have a view controller that segues to a second view controller which loads several images but it hangs for a second or two before seguing from the first VC to the second. I am trying to add a UIActivityIndicatorView so that the user doesn't think the app is frozen (which is currently what it feels like). However I can't seem to get it to work properly and all of the examples I've seen are using a web view or are accessing some kind of data from a server whereas I'm loading images that are stored in the app.
I have some code below to show what I have attempted.
.h file
#interface SecondViewController: UIViewController
#property (strong, nonatomic) UIActivityIndicatorView *indicator;
.m file
-(void)viewWillAppear:(BOOL)animated
{
self.indicator = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray];
self.indicator.center = CGPointMake(160, 240);
[self.view addSubview:self.indicator];
//Loading a lot of images in a for loop.
//The images are attached to buttons which the user can press to bring up
//an exploded view in a different controller with additional information
[self.indicator startAnimating];
for{....}
[self.indicator stopAnimating];
}
I have tried also using dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0) immediately after the call to [self.indicator startAnimating] but all that happened was that the view controller loaded instantly and the images/buttons never loaded at all.
How can I get rid of the delay when the user clicks the "next" button on the first view controller? The app hangs on the first VC for about a second or two then finally loads the second view controller with all the images/buttons. Do I need to add the UIActivityIndicatorView to the first view controller instead or am I going about this completely the wrong way? I'm open to any and all methods to get this done, thanks in advance.
You need to call the initialization code and stopAnimating in the next run loop. One easy thing you can do is the following:
-(void)viewWillAppear:(BOOL)animated
{
self.indicator = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray];
self.indicator.center = CGPointMake(160, 240);
[self.view addSubview:self.indicator];
//Loading a lot of images in a for loop.
//The images are attached to buttons which the user can press to bring up
//an exploded view in a different controller with additional information
[self.indicator startAnimating];
[self performSelector:#selector(loadUI) withObject:nil afterDelay:0.01];
}
-(void) loadUI {
for{....}
[self.indicator stopAnimating];
}
Of course there are other ways to run loadUI in the next run loop (such as using a timer).
Related
I am trying to build a simple library with working UIElements. What I am trying to do is, creating UIViewController objects from one class instances and push that new ViewController on the current VC Stack with the presentViewController method.
I can see that the UIElements has been successfully adding on the stack, but GestureRecognizer and UIButton's target does not work. When I am checking on ViewDebug, these settings are <NSNull null>.
This is my class method which I am creating the UI and putting on the current view stack.
-(void)displayAd{
dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void){
//Background Thread
NSData * imageData = [NSData dataWithContentsOfURL:[NSURL URLWithString:fullpageCampaign.mainImage]];
dispatch_async(dispatch_get_main_queue(), ^(void){
//Run UI Updates
fullPageView = [[UIViewController alloc] init];
fullPageView.view.frame = CurrentVC.view.bounds;
fullPageView.view.backgroundColor = [UIColor redColor];
UIImageView *staticImageView = [[UIImageView alloc] init];
staticImageView.frame = CurrentVC.view.frame;
UITapGestureRecognizer *singleTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(tapDetected)];
singleTap.numberOfTapsRequired = 1;
[staticImageView addGestureRecognizer:singleTap];
staticImageView.userInteractionEnabled = YES;
[fullPageView.view addSubview:staticImageView];
staticImageView.image = [UIImage imageWithData:imageData];
[CurrentVC.view addSubview:fullPageView.view];
//[fullPageView didMoveToParentViewController:self];
[CurrentVC presentViewController:fullPageView animated:YES completion:^{
NSLog(#"Tagon Ads is about to showing.");
UIButton *closeButton = [self createButtonWithAssetName:#"tagonAssets.bundle/close_button" TargetMethod:#"closeModal" andView:staticImageView];
[staticImageView addSubview:closeButton];
[CurrentVC.view bringSubviewToFront:closeButton];
}];
});
});
}
CurrentVC is the current viewController that I am sending as a parameter through my library's method in order to add a new viewController stack on to it.
Where is closeModal action? Probably same class as your currentVC. If so, your closeButton referenced currentVC but you already gone to fullPageView from there. So, your button lost his reference.
Just create new controller, send imageData there, create custom initializer, create new UIImageView and UIButton in there. With this way, your button gonna be reference own root and your problem should be solved.
There are several problems with your code, but first of all, I would recommend a different approach to accomplish what you want. As you can see below, using an instance of UIViewController is not the recommended way. Instead, use a storyboard to set up your view controller and it's components. Your code will be much smaller and your design will be easy to understand and change.
You can read more about UIViewController here
You rarely create instances of the UIViewController class directly.
Instead, you create instances of UIViewController subclasses and use
those objects to provide the specific behaviors and visual appearances
that you need.
Here is another potential. Is fullpageCampaign.mainImage residing remotely or locally? If remotely located, then you should consider changing to NSURLSession instead.
Read more about NSData:dataWithContentsOfURL here
Do not use this synchronous method to request network-based URLs. For
network-based URLs, this method can block the current thread for tens
of seconds on a slow network, resulting in a poor user experience, and
in iOS, may cause your app to be terminated.
Another minor thing is that you add the button to the image view. While this is OK, and might work when you allow user interaction for the image view, a cleaner way to do it is to create a UIView container to hold the image view and the button. The container can then also be the view that you attach the tap gesture recognizer to. That way, the image view can stay as a pure image.
The storyboard approach
First, create a sub-class of UIIViewController. It should look something like this:
FullPageViewController.h
#import <UIKit/UIKit.h>
#interface FullPageViewController : UIViewController
- (void)setImage:(UIImage *)adImage;
#end
FullPageViewController.m
#import "FullPageViewController.h"
#interface FullPageViewController ()
#property (weak, nonatomic) IBOutlet UIImageView *adImageView;
#end
#implementation FullPageViewController
- (void)setImage:(UIImage *)adImage {
self.adImageView.image = adImage;
}
- (IBAction)tappedOnAd:(UIGestureRecognizer *)sender {
if (sender.state == UIGestureRecognizerStateEnded) {
// Do your ad thing here
}
}
- (IBAction)closeButtonPressed:(UIButton *)sender {
[self dismissViewControllerAnimated:YES completion:nil];
}
#end
Second, create a storyboard and add your ad view controller to it. Then add an image view, a button, and a tap gesture recognizer to your view controller. The tap gesture recognizer should be dropped on the image view to capture taps from there. You pull all of these objects from the Object Library down right. Also remember to enable user interaction for the image view. There is a property for that on the property page.
You should now have something that looks like this:
Notice the class name top right which should be the name of your new view controller class you just created. Also notice the storyboard ID adVC which you need when instantiating the view controller from code.
The next step is to connect the objects. Select the image view, then drag from the outlet (the ring) under Referencing Outlets to the view controller icon (the yellow icon with a square in) located on top of the view controller window, and select adImageView. The gesture recognizer should already be connected, if you dropped it on the image view when you placed it previously.
Next, connect the action for the close button. Drag from the Touch Up Inside outlet to the view controller icon (the yellow one), and select the closeButtonPressed: method.
Next, connect the tap gesture recognizer to your code. Select it from the list on the left, then drag from Sent Actions to the view controller icon and select tappedOnAd:.
Finally, your code to show the ad looks something like this. This method belongs in your parent view controller.
-(void)displayAd{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void){
//Background Thread
NSData * imageData = [NSData dataWithContentsOfURL:[NSURL URLWithString:fullpageCampaign.mainImage]];
dispatch_async(dispatch_get_main_queue(), ^(void){
//Run UI Updates
UIStoryboard *storyboard = [UIStoryboard storyboardWithName:#"AdPage" bundle:nil];
UIViewController *vc = [storyboard instantiateViewControllerWithIdentifier:#"adVC"];
[vc setImage:[UIImage imageWithData:imageData]];
[self presentViewController:vc animated:YES completion:^{}];
});
});
}
I am somewhat new to ios but I've been able to muddle though... until now. I have an application with a login page. First thing I did was create a few empty view controllers and stuck them on a storyboard. I have a LoginViewController with some text fields for userId and password plus a login button. Plan is if you successfully log in you are brought to a TabViewController. Right now this is out of the box. I deleted the two view controllers that got created with it and replaced them with two NavigationControllers.
Just to test everything I made a segue from the login button to the TabViewController. Everything worked fine. Views came up. All the out of the box stuff worked.
Next step I tried to simulate an actual login. Since I have to do this through a web service call I figured it needed to be asynchronous. I deleted the initial segue I added for the login button and added a IBAction from the button to my LoginViewController. I also added a manual segue from my LoginViewController to the TabViewController and I named it "loginSegue"
Here is the code I have so far:
- (IBAction)login:(id)sender {
[Decorator showViewBusyIn:self.aView
scale:1.5
makeWhite:NO];
self.clientIdText.enabled = NO;
self.userIdText.enabled = NO;
self.passwordText.enabled = NO;
UIButton* loginBtn = sender;
loginBtn.enabled = NO;
[Decorator showViewBusyIn:self.aView
scale:2.0
makeWhite:NO];
self.operation = [[NSInvocationOperation alloc]
initWithTarget:self
selector:#selector(doLogin)
object:nil];
self.queue = [[NSOperationQueue alloc] init];
[self.queue addOperation:self.operation];
}
-(void)doLogin{
[NSThread sleepForTimeInterval:1];
[Decorator removeBusyIndicatorFrom:self.aView];
// this is where I will eventually put the login code...
[self performSegueWithIdentifier:#"loginSegue" sender:self];
}
I put the call to sleepForTimeInterval to simulate waiting for the web service call to complete. I will remove it later. The Decorator stuff just shows and removes an Activity Indicator View.
When I do all this the segue works but the view associated with the login view controller remains on the screen. Put another way, the TabViewController shows up. The first item is selected. The NavigationController shows up but the VC associated with it and the view it contains does not appear. The view from the LoginViewController stays there.
Since all navigation worked fine when I put the segue on the login button I'm thinking it has something to do with the invocation operation. Either that or somehow my view or view controller hierarchy is getting messed up.
Any ideas as to what I'm doing wrong?
Is this a good way to do a login?
Any help is much appreciated,
Nat
For this kind of operation, using the GCD can be easier. You would do something like:
- (void)doLogin
{
dispatch_queue_t loginQueue = dispatch_queue_create(“login”, NULL);
dispatch_async(loginQueue, ^{
// this is where you will eventually put the login code...
dispatch_async(dispatch_get_main_queue(), ^{
[Decorator removeBusyIndicatorFrom:self.aView];
[self performSegueWithIdentifier:#"loginSegue" sender:self];
});
});
}
And in your -(IBAction)login:(id)sender you simply call [self doLogin] instead of
self.operation = [[NSInvocationOperation alloc]
initWithTarget:self
selector:#selector(doLogin)
object:nil];
self.queue = [[NSOperationQueue alloc] init];
[self.queue addOperation:self.operation];
Check this question, which briefly explains what are the main differences between GCD and NSOperationQueue
I'm transitioning between my game's main menu and the game itself, and am in need of a loading screen. The current behavior is that I tap the Play button, and there's no movement and no segue for a few seconds, then the game begins.
Temporarily, I'm just throwing up a UIActivityIndicatorView and manually telling it to startAnimating, then calling the segue to the other scene. The issue is that code from the game view (the end-point of the segue) is being run before the view has physically changed on-screen (i.e. I'm seeing the menu while code from the game is being executed). I don't quite see how this happens, since the spinner is told to display before the segue is called. Here's the code:
- (IBAction) startGame: (id) sender
{
[_loadingView setHidden: NO];
[_loadingSpinner startAnimating];
[self performSegueWithIdentifier: #"PlayGame" sender: self];
}
The stuff that's actually taking the time isn't called until the segue has completed (i.e. until the game's main view is shown onscreen in an entirely different ViewController (it's querying an SQLite database)), so why is the spinner not showing until after that work has been done? Should I be using GCD or similar for this? I've done a little experimenting with it but it might be a tad above my level at present. The heavy code is contained in one method, so I'd ideally like to be able to do something like this, literally executing the lines in order.
[spinner startAnimating];
[self callMethodThatTakesAges];
[spinner stopAnimating];
I've tried using performSelectorOnMainThread:withObject:waitUntilDone:, but it seems to be getting confused between the two views. The spinner should show on the game view, not the menu view, ideally, so I should tap Play, the transition to the game view should be instant, then the spinner should appear, loading should start, spinner should vanish when done.
Any ideas? Thanks!
EDIT: Here are some methods from my game view controller (makeNewWord is the troublesome method).
- (void) viewDidAppear: (BOOL) animated
{
UIActivityIndicatorView *spinner = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle: UIActivityIndicatorViewStyleWhiteLarge];
[spinner setCenter: CGPointMake(240, 55)];
[self.view addSubview: spinner];
[spinner startAnimating];
[self makeNewWord];
[spinner stopAnimating];
[self becomeFirstResponder];
}
- (void) viewDidLoad
{
[super viewDidLoad];
[self canBecomeFirstResponder];
firstLetterMultiplierLetter = [[NSString alloc] init];
userIsEnteringAWord = NO;
[self setUpGestures];
}
Here's the method called from my main menu view that segues into the game view:
- (IBAction) startGame: (id) sender
{
[self performSegueWithIdentifier: #"PlayGame" sender: self];
}
It's hard to tell what you have where from your question. I would say you want to do this:
1) add the spinner to the game view
2) in game view viewDidAppear, have [spinner startAnimating];
3) call [self methodThatTakesAges] (as the next line in viewDidAppear).
4) call [spinner stopAnimating] from whatever call back method you have in methodThatTakesAges
The only thing in your startGame: method should be the performSegue line. You might also need to change where you start the download to where I said, it's not clear where you're starting that now.
I am trying to get a popup effect and want to design the popup view in another view controller so i can use the xib to do it.
When i used the presentViewController or pushViewController and set the background to transparent, i end up seeing the Window's background color.
I tried this code to add subview to the navigation controller's view so that i can have the Info view cover the entire screen with a transparent background. I also have tab bar to cover up as well.
InfoVC *vc = [[InfoVC alloc] initWithNibName:#"InfoVC" bundle:nil];
[self.navigationController.view addSubview:vc.view];
My problem is inside my InfoVC when i try to dismiss it, the app will crash with some EXC_BAD_ACCESS message:
[self.view removeFromSuperview];
EDIT:
I found a way to stop it crashing but setting the InfoVC as a property in the MainVC. I think the reason for crash is when i call "self.view" in the action inside the InfoVC, it doesn't know that self is the InfoVC inside MainVC.
InfoVC *vc = [[InfoVC alloc] initWithNibName:#"InfoVC" bundle:nil];
[self.navigationController.view addSubview:vc.view];
No no no no. Never never do that.
There is an elaborate dance that you must traverse in order to put a view controller's view inside another view controller's view (or remove it afterwards) if it doesn't come with built-in facilities for doing this (the way a UISplitViewController does, or the way a navigation controller manages the views of the view controllers that are pushed and popped within it).
Read up on customer container controllers. One of the examples from my book is here:
https://github.com/mattneub/Programming-iOS-Book-Examples/blob/master/ch19p556containerController/p476containerController/ViewController.m
Shouldn't you be using the following to remove the view from its superview?
[vc.view removeFromSuperview];
You can never have a UIView remove it's subviews, the subviews themselves must remove themselves from it's superview. You can easily loop through subviews and have them removed like so
for (UIView *view in vc.view.subviews) {
[view removeFromSuperview];
}
Docs for reference:
http://developer.apple.com/library/ios/#documentation/uikit/reference/uiview_class/uiview/uiview.html
After a "modally" presented view controller has appeared the views under the now presented view controller will be removed; this saves memory, and eases rendering. In your case, though, you also end up seeing the window behind the "modally" presented view.
The natural, and seemingly logical, next step is to simply take one view controller's view and cram it into another. However, as you have discovered, this is problematic. With the newly inserted view safely retained by the view hierarchy it is safe, but the new view controller is not so lucky, it is quickly deallocated. So when this new view tries to contact its controller you will get an EXC_BAD_ACCESS and crash. One workaround, again as you have found, is to simply have the original view controller keep a strong reference to the new view controller. And this can work... badly. There's still a good chance you will get an UIViewControllerHierarchyInconsistencyException.
Of course if you simply want to add a small view you create in IB you don't need to use a view controller as the "File's Owner" and there are many examples of creating an instance of a view from a xib file.
The more interesting question here is, "How would/does apple do it?" Apple consistently says that a view controller is the correct controller for an encapsulated unit of work. For example, their TWTweetComposeViewController, you present it, and it seems to float. How?
The first way of accomplishing this that comes to my mind is to have a clear background that isn't clear. That is, create an image of the screen before the presented view controller appears and set that as the background before the presenting view is removed. So for example(Explanation to follow):
QuickSheetViewController.xib
QuickSheetViewController.h
#import <UIKit/UIKit.h>
#interface QuickSheetViewController : UIViewController
- (IBAction)dismissButtonPressed:(id)sender;
#end
QuickSheetViewController.m
#import "QuickSheetViewController.h"
#import <QuartzCore/QuartzCore.h>
#implementation QuickSheetViewController {
UIImage *_backgroundImage;
}
-(void)renderAndSaveBackgroundImageFromVC:(UIViewController *)vc{
UIGraphicsBeginImageContext(vc.view.bounds.size);
[vc.view.layer renderInContext:UIGraphicsGetCurrentContext()];
_backgroundImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
}
-(void)viewWillAppear:(BOOL)animated{
[super viewWillAppear:animated];
// save an image of the current view, and set our background to clear so we can see the slide-in.
[self renderAndSaveBackgroundImageFromVC:self.presentingViewController];
self.view.backgroundColor = [UIColor clearColor];
}
-(void)viewDidAppear:(BOOL)animated{
[super viewDidAppear:animated];
// Time to use our saved background image.
self.view.backgroundColor = [UIColor colorWithPatternImage:_backgroundImage];
}
-(void)viewWillDisappear:(BOOL)animated{
[super viewWillDisappear:animated];
// Set our background to clear so we can see the slide-out.
self.view.backgroundColor = [UIColor clearColor];
}
- (IBAction)dismissButtonPressed:(id)sender {
[self dismissViewControllerAnimated:YES completion:nil];
}
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation{
return (interfaceOrientation == UIInterfaceOrientationPortrait);
}
#end
The majority of this example hinges upon the renderAndSaveBackgroundImageFromVC: method. In which, we create a graphics context render the view we are about to cover into it, and then create a UIImage to later (in viewDidAppear) use as a background.
Now simply use it like:
QuickSheetViewController *newVC = [[QuickSheetViewController alloc] initWithNibName:nil bundle:nil];
[self presentViewController:newVC animated:YES completion:nil];
You will see through the background just long enough for the animation to happen, then we use our saved image to hide the removal of the presenting view.
I am confused as to Storyboards in iOS. I want to use them and have my code be as modern as possible, but somewhat confused. Here is my problem:
In the main view of my app, you click a button and some time-consuming things happen behind the scenes (music plays, some files are concatenated). While this is happening, I want a menu with some text fields to slide up and let the user enter some info, and then he will click a button to dismiss this menu.
This menu can cover the full screen, but I don't want to segue to a new Storyboard entirely, because there is stuff going on in the background and we'll need to come back to the main view soon.
One thing I tried was to create a new storyboard for this menu and load it using instantiateViewControllerWithIdentifier, which works, but then I can't dismiss it later without the app crashing. But maybe there is a better way anyway? What is proper programming style on this?
Someone asked about my code, I think this is the only place I can put it? Here it is. In the main view:
AddInfoController *infoSheet = [[AddInfoController alloc] init]; //subclass of viewcontroller
infoSheet = [self.storyboard instantiateViewControllerWithIdentifier:#"AddInfoView"];
[self.view addSubview:infoSheet.view];
then in my AddInfoController class I have:
- (IBAction)clickedDoneButton:(id)sender {
[self removeFromParentViewController];
}
and in the AddInfoView storyboard, I have a button which is hooked to that IBAction. When it crashes, nothing appears in NSLog, it shows me some hex stuff in Thread 1 and a EXC_BAD_ACCESS error
Ok - if you are bent on using Storyboards and the app keeps crashing when you present the dismiss button, there's most certainly an error you are making. The console error message would be helpful. What are you seeing there.
If Storyboards is taking too much of my time, I would do it programmatically and simply present a Modal View. If you haven't tried before, try presenting a view controller modally. You can learn more about it here.
Create a UIView and UITextViews programmatically and them to the main view. Try something like this:
UIView *someView = [[UIView alloc]initWithFrame:CGRectMake(20, 20, 280, 420)];
UITextField * someTextField = [[UITextField alloc]initWithFrame:CGRectMake(20, 20, 240, 44)]; // create uitextfield or something else as much as you want.
[someView addSubview:someTextField];
[someTextField release];
[self.view addSubview:someView];
[someView release];
And create a action button to dismiss the view.
It looks like you are calling removeFromParentViewController but you never actually add it as a child. I don't know if that is what is actually calling the crash, but basically the format to use is:
taken from Apple:
- (void) displayContentController: (UIViewController*) content
{
[self addChildViewController:content]; // 1
content.view.frame = [self frameForContentController]; // 2
[self.view addSubview:self.currentClientView];
[content didMoveToParentViewController:self]; // 3
}
Then after you are finished you call the equivalent methods in reverse:
- (void) hideContentController: (UIViewController*) content
{
[content willMoveToParentViewController:nil]; // 1
[content.view removeFromSuperview]; // 2
[content removeFromParentViewController]; // 3
}