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
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 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).
I have view controller I want to load different web pages on button clicks.
I have single view controller where i can load web page.
Button click events
- (IBAction)btnResortTVTouch:(id)sender {
GlobalWebViewController *globalWebViewController1 = [[GlobalWebViewController alloc] init];
globalWebViewController1.strUrlName = #"http://www.youtube.com/user/xyz";
[self presentViewController:globalWebViewController1 animated:YES completion:nil];
[globalWebViewController1 selectPageLink];
}
- (IBAction)btnPIntrestTouch:(id)sender {
GlobalWebViewController *globalWebViewController = [[GlobalWebViewController alloc] init];
globalWebViewController.strUrlName = #"http://www.pinterest.com/xyz/";
[self presentViewController:globalWebViewController animated:YES completion:nil];
[globalWebViewController selectPageLink];
}
It gives error
2013-12-19 03:00:17.885 RWNewYork[5941:907] Warning: Attempt to present GlobalWebViewController: 0x80c8e90 on FiveViewController: 0x81af490 which is already presenting GlobalWebViewController: 0x80826e0
You probably copied the first button, that is how you created the second one in the storyboard, and now it has 2 separate actions. So for one of your buttons both methods are called. Right-click on them in the storyboard and you can see the actions attached.
Edit:
Maybe you should call the selectPageLink method from the completion block of the presentation.
It's telling you that you've already presented a GlobalWebViewController from your FiveViewController, and you're trying to present another GlobalWebViewController from it as well, which isn't possible. So your button press must be somehow calling both methods, or calling one of them twice.
I have a view that requires user to be logged in. When the user attempts to open that view an he is not logged in I will call the login view for him to login and after he is done I will call the original view that he intended to see.
On iPhone this works fine as I push view controllers there.
But on iPad where I present view controller this does not work. It says that dismissal in progress, can't show new controller. Here is the code:
- (void) buttonPressed
{
if (!userLoggedIn) { // userLoggedIn getter calls new screens of login if needed
return; // this is executed if user declined to login
}
MyViewController *temp = [[MyViewController alloc] init];
[self.navigationController presentViewController:temp animated:YES]; // this returns warning that dismissal in progress and does not work
}
What can I do about that? On iPhone all of my logic works fine, but on iPad it fails. I use it in many places and completely rewriting code is not good.
EDIT: more code:
- (BOOL) userLoggedIn {
// code omitted
[centerController presentViewController:navController animated:YES completion:nil];
// code omitted
[centerController dismissViewController:navController animated:YES]; // setting to NO does not fix my problem
return YES;
}
EDIT2:
This is the code for iPad. I have removed iPhone-related code. What it does on iPhone - instead of presenting controller it uses pushing, and in that situation everything works fine.
You cannot present another view as long as the dismissing of your 1st view is not complete. The animation of dismissing view should be completed before presenting new view. So, either you can set its animation to NO while dismissing, or use
performSelector:withObject:afterDelay:
and present the next view after 2-3 seconds.
Hope this helps.
You've not posted enough code to really see what you're doing, but one approach to the problem of dismissing and pushing view controllers clashing in this way is to make a the pop+posh into a single atomic operation operation, rather then seqential operations.
You can do this by using the setViewControllers:animated: method on UINavigationController. This allows you to effectively remove one or more view controllers, and add one or more view controllers, all as one cohesive operation, with one seamless animation.
Here's a simple example:
[self.navigationController pushViewController:loginController];
// ... later on, when user login is validated:
NSMutableArray *viewControllers =
[self.navigationController.viewControllers copy];
[viewControllers removeLastObject];
[viewControllers addObject:[[MyNewViewController alloc] init]];
[self.navigationController setViewControllers:viewControllers animated:YES];
If you do things this way, your code will be more predictable, and will work across iPhone and iPad.
For more info, see the API docs.
Update
Since your problem involves a modal dialog on top, try using setViewControllers:animated:NO to change the nav controller stack underneath the modal login dialog before you dismiss the modal.
I am creating an iPhone client for one of my apps that has an API. I am using the GTMOAuth2 library for authentication. The library takes care of opening a web view for me with the correct url. However I have to push the view controller myself. Let me show you some code to make things more clear:
- (void)signInWithCatapult
{
[self signOut];
GTMOAuth2ViewControllerTouch *viewController;
viewController = [[GTMOAuth2ViewControllerTouch alloc] initWithAuthentication:[_account catapultAuthenticaiton]
authorizationURL:[NSURL URLWithString:kCatapultAuthURL]
keychainItemName:kCatapultKeychainItemName
delegate:self
finishedSelector:#selector(viewController:finishedWithAuth:error:)];
[self.navigationController pushViewController:viewController animated:YES];
}
I have a "plus"/"add" button that I add to the view dynamically and that points to that method:
self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAdd target:self action:#selector(signInWithCatapult)];
When I press the "add" button, what is supposed to happen is to open the web view with an animation, and then add an account to the accounts instance variable which populates the table view. This works fine if I add one account, but as soon as I try to add a second account, the screen goes black and two errors appear in the console:
nested pop animation can result in corrupted navigation bar
Finishing up a navigation transition in an unexpected state. Navigation Bar subview tree might get corrupted.
The only way that I found to avoid this problem was to disable animations when pushing the view controller.
What am I doing wrong please?
Typical situations
You push or pop controllers inside viewWillAppear: or similar methods.
You override viewWillAppear: (or similar methods) but you are not calling [super viewWillAppear:].
You are starting two animations at the same time, e.g. running an animated pop and then immediately running an animated push. The animations then collide. In this case, using [UINavigationController setViewControllers:animated:] must be used.
Have you tried the following for dismissing once you're in?
[self dismissViewControllerAnimated:YES completion:nil];
I got the nested pop animation can result in corrupted navigation bar message when I was trying to pop a view controller before it had appeared. Override viewDidAppear to set a flag in your UIViewController subclass indicating that the view has appeared (remember to call [super viewDidAppear] as well). Test that flag before you pop the controller. If the view hasn't appeared yet, you may want to set another flag indicating that you need to immediately pop the view controller, from within viewDidAppear, as soon as it has appeared. Like so:
#interface MyViewController : UIViewController {
bool didAppear, needToPop;
}
...and in the #implementation...
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
didAppear = YES;
if (needToPop)
[self.navigationController popViewControllerAnimated:YES];
}
- (void)myCrucialBackgroundTask {
// this task was presumably initiated when view was created or loaded....
...
if (myTaskFailed) { // o noes!
if (didAppear)
[self.navigationController popViewControllerAnimated:YES];
else
needToPop = YES;
}
}
The duplicated popViewControllerAnimated call is a bit ugly, but the only way I could get this to work in my currently-tired state.