Currently I have two flows laid in storyboard: the onboarding / login flow (currently set as the initial view controller in storyboard settings) and the main application flow (e.g. the core application experience after you're logged in).
The transition from onboarding to the main app flow happens successfully in the UI, but I don't think the way I'm doing it is quite right. One problem I'm having is the inability to restore view state to the main application flow.
When transitioning from the onboarding flow to the main app flow, I'm using presentViewController to transition to the new view controller, and then I'm removing the presenting view controller's parent navigation controller from it's view hierarchy using removeFromParentViewController. My idea here is that the main app flow should really have it's own view hierarchy, the root of which should be the first view in the newly presented view controller.
However, doing things in this manner breaks the view restoration code. It seems I'm somehow failing to properly "reset" the view hierarchy to the main navigation controller. View state restoration was working before when I was not removing the onboarding navigation controller from the view hierarchy (as in the code below), but now it's not working -- state is always restored back to the beginning of the onboarding flow, the - (void)decodeRestorableStateWithCoder:(NSCoder *)coder method is never called on the main app flow controller.
Below is the relevant code from the onboarding flow controller that presents the main app flow controller.
// The presenting view controller from the onboarding flow
#implementation TLOnboardingStep2ViewController
-(void)presentMainViewController {
UIStoryboard *board = [UIStoryboard storyboardWithName:#"Main" bundle:nil];
// The root view controller of the main app flow
ECSlidingViewController *slidingVC = [board instantiateViewControllerWithIdentifier:#"slidingViewController"];
[self presentViewController:slidingVC animated:YES completion:^{
[[UIApplication sharedApplication].delegate window].rootViewController = slidingVC;
[self.navigationController removeFromParentViewController];
}];
}
#end
What am I doing wrong here? Thanks.
It seems like a better way to handle this is to make the main application flow the root view controller, and pop into the login flow as a modal view controller from the main view controller when needed. This is what I'm doing now and it seems to be working a lot better, and keeping the hierarchy much cleaner.
Related
I have an application where the user has to enter some information on the initial screens, such as the login credentials. All the navigations are done using a storyboard and segues, which automatically sets the navigation bar on top.
I have two storyboards that share the same controllers, so I use the same name for the segues (ipad and iphone versions).
So when the user comes back to the application, I read the core data and know that he has already performed the initial steps, so I would like to "skip" those screens.
Problem:
I can only execute the segues after the view is visible, otherwise the navigation is screwed up. But when doing so, the user sees the screen briefly and sees the animation "pushing" that screen away. I'd like to keep the navigation history on the navigation bar, that is why I want to use the segues and all the logic associated with them.
All the solutions point to creating the views programatically and putting them on the stack, but I'd like to take advantage of the storyboards.
Here is one way of doing it;
In your StoryBoard, assign an identifier to the second view controller (this is done on the identity inspector, setting the Storyboard ID field). In the code below, i have named mine secondVC;
Then in the viewDidLoad for your first controller (the one you want to skip but come back to) do something like this;
- (void)viewDidLoad{
[super viewDidLoad];
/// validate viewController one being displayed
if(dontDisplayFirstController){
UIStoryboard *storyBoard = self.storyboard;
UIViewController *targetViewController = [storyBoard instantiateViewControllerWithIdentifier:#"secondVC"];
UINavigationController *navController = self.navigationController;
if (navController) {
[navController pushViewController:targetViewController animated:NO];
}
}
}
This will efectivly push to the second viewController whilst still maintaining viewController one in the navigation tree.
I am writing a turn-based game for the iOS platform. The client communicates with a remote server using the CocoaAsyncSocket API. Right now I work on this case: the client has been inactive for a while, and has been disconnected from the server due to timeout. If that's the case, I wish to pop back to the login view when the app enters the foreground, to let the user log back in again.
I assume I have to do this kind of work in the -(void)applicationWillEnterForeground of my app's delegate. Checking whether I'm connected or not is not a problem, but I don't know how to dismiss every presenting view controller to take me back to the root view (which happens to be the login view).
My view controllers are presented modally, but I also have two navigation controllers with table views including push segues.
Any help with this problem is highly appreciated. Thanks in advance!
The jarring way to do it is just replace the rootViewController with a new login view controller.
- (void)applicationWillEnterForeground:(UIApplication *)application
{
if ([self isDisconnected]) {
self.window.rootViewController = [MyLoginController new];
}
}
For storyboards, assuming your initial storyboard is the login storyboard.
- (void)applicationWillEnterForeground:(UIApplication *)application
{
if ([self isDisconnected]) {
UIStoryboard *storyboard = self.window.rootViewController.storyboard
self.window.rootViewController = [storyboard instantiateInitialViewController];
}
}
Depending on your UX, this may or may not be appropriate.
If all of your content views are presented modally from the root view controller (which doesn't sound like a great idea) then from the app delegate (indeed in applicationWillEnterForeground),
// if we need to login
UIViewController *rootViewController = self.window.rootViewController;
[rootViewController dismissViewControllerAnimated:YES completion:nil];
If the root view controller isn't presenting modally then you need to dismiss in the appropriate way. If any other 'child' view controller is presenting modally then it should dismiss. I guess dismissing when that view controller changes parent view controller will work. You may need to add a notification to inform all view controllers that everything is being torn down.
The main view of my app is a UIImagePickerController camera view.
When the app becomes active (in didBecomeActive), I present a modal view controller that shows some settings generated from a network request. (Note that for debugging purposes, I took the network request out and am currently just showing a dummy view)
The modal view animates in smoothly, but after loading it freezes for 3 seconds then responds normally. After dismissing the view (also animates smoothly), my image picker controller pauses for 2 seconds then resumes normally.
I have removed all functionality from the modal view controller to make sure there was no operations clogging the main thread. I am presenting the most basic of controllers, and still get the choppy ui. I would suspect that this is from my presenting view controller calling viewDidLoad/Unload or something similar, but my search did not give me any information on what delegate methods are called in the presenting view controller when a modal view is shown.
My problem can be solved by answering:
What delegate methods are called in the presenter when a modal view is shown?
(If any ^) How can I not call those methods, or make them run smoother?
What common pitfalls are associated with modal view controllers?
This is probably because you are making a lot of processing in the main thread (usually when UI stops, it's because main thread processing). Try to provide us some code, specifically the one you think is the most heavy processing code! Sorry about my poor english :P!
Try dispatching most heavy code to another thread with
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//your heavy code here =)
});
Regards,
Lucas
There are multiple methods invoked. ViewDidLoad ViewWillAppear ViewDidAppear ViewWillDisappear ViewDidDisappear. Check all of those methods. Also, check any subviews you have created and see if they are doing any thing on their thread involving image loading in the methods i stated. Also does this occur in the simulator as well as a test device?
ModalViewControllers do not have too many pitfalls but understanding how many views are allocated on things like navigation stacks and how many views you have on top of each other. When you get rid of the modal viewcontroller do you call dismissviewcontroller?
One thing that might be a contributor to some slight lag is reloading the same viewController from scratch each time.
BProfileTableViewController * _profileViewController = [[UIStoryboard storyboardWithName:#"Profile" bundle:[NSBundle chatUIBundle]] instantiateInitialViewController];
UINavigationController * profileNavigationController = [[UINavigationController alloc] initWithRootViewController:profileViewController];
[self.navigationController presentViewController:profileNavigationController animated:YES completion:nil];
You can see here that if this is on a tableView click then each time the app needs to create the viewController again. If instead we just reuse the view then it gets rid of some of that lag.
Add this in the header file
BProfileTableViewController * _profileView;
Then the modal view load code changes to:
// Open the users profile
if (!_profileView) {
_profileView = [[UIStoryboard storyboardWithName:#"Profile" bundle:[NSBundle chatUIBundle]] instantiateInitialViewController];
}
UINavigationController * profileNavigationController = [[UINavigationController alloc] initWithRootViewController:_profileView];
[self.navigationController presentViewController:profileNavigationController animated:YES completion:nil];
This means we are just reloading the view instead of recreating the view
Try
layer.masksToBounds = true
I know this question is a bit open-ended but I'm trying to make it as specific as possible because I don't really know what a standard convention would be for this type of thing with iOS. I have been looking into implementing a login screen on my app, however I am having trouble figuring out the best way. I have three methods listed below, could anybody tell me if one of these is better or a more correct approach? (or if there is something that I totally missed).
The first approach - I initially have a navigation controller as my root controller with the login screen being the first view on the stack. Then when the user logs in, i just push the main UI to the stack. The user can then use the back button to return to the login screen. I know this technique works but then when re-launching the app, the user doesn't need to login again. I'm not sure should I just set up the stack the same way (login page first) and then push the main UI on top? Is that a common technique?
The next approach, I found here how to replace the root controller How to change RootViewController (in AppDelegate) from within CustomViewController? I like this idea better where I could just get rid of the login screen after login and add some type of button to bring it back on logout. Then during re-launch I can just skip adding the login screen altogether.
The third approach is perhaps overlaying the login on top of the main UI but I haven't found any code to do this. It also seems a little weird because my main UI might start firing messages that i wouldn't want to occur until after login so I'd have to account for that.
I would suggest you to have a root view controller with a blank view. This controller is set as a root view controller in the AppDelegate's applicationDidFinishLaunching: method.
Role of this root view controller would be to show the login screen through login view controller initially. Login view controller delegates to root view controller upon successful login to remove itself from root view and root view controller then set the next controller to be displayed.
This way you can manages multiple views if any before showing the main view through your root view controller.
There are different ways to display login view on top of root view.
1) Present a login view controller without animation.
2) Add login view as a subview on top of root view [Note: Before iOS 5, you don't get the rotation and view life cycle call backs and you should write your own code to pass the callbacks to the view added on top of root view. From iOS 5 onwards you have view containment concept using which you can maintain parent child relationship.]
Hope this helps.
We have a similar app. Our storyboard looks a little like a wheel.
We have a navigation controller as our root controller, which then goes to a view controller with a spinner on it. That controller handles the call to see if the user is logged in or not. It is connected to two different controllers.
If they are already logged in, it goes to the main view controller. If they haven't logged in, it goes to the login view controller. The login view controller will go back to initial view controller on success, and since they'll then be logged in, it goes to the main controller.
To fall back, we go all the way back to the initial view controller with the spinner on it.
You can hide the navigation bar on every view before the main one.
I always implement it with storyboard. Just add Storyboard ID to your login view controller, say Onboarding.
Then write something like this in application: didFinishLaunchingWithOptions:
if (![[NSUserDefaults standardUserDefaults] objectForKey:TRAIN_COMPLETE]) {
UIStoryboard* storyboard = [UIStoryboard storyboardWithName:#"Main" bundle:nil];
self.window.rootViewController = [storyboard instantiateViewControllerWithIdentifier:#"Onboarding"];
}
All our ViewController are subclassed from a custom subclass of uiviewcontroller. This allow us to make powerful enhancements on behavior, constraints, ...
Our super view controller class handle authentification by summoning a modal view on viewWillAppear if user is not logged in.
Works like a charm to handle disconnection and resuming from any point in the app.
When a UIViewController presents another view controller the simplest way for the presented view controller to dismiss itself when it is done under iOS 5 is to call:
[[self presentingViewController] dismissViewControllerAnimated:YES completion:NULL];
On the other hand, Apple's View Controller Programming Guide says:
When it comes time to dismiss a presented view controller, the preferred approach is to let the presenting view controller dismiss it. In other words, whenever possible, the same view controller that presented the view controller should also take responsibility for dismissing it. Although there are several techniques for notifying the presenting view controller that its presented view controller should be dismissed, the preferred technique is delegation.
This has led some answers here to suggest sticking with making a new protocol and delegation even when only a very simple view controller is being presented. Why is this the documentation's "preferred technique" as opposed to the single line above? Is there any offsetting advantages to downside of a large increase in code written with the delegate/protocol technique? Obviously if there is information from the presented view controller that needs to be passed back to the presenting view controller delegation is a good technique. However, the information is the reason for delegation, not simply cleanly removing the presented view controller from the screen.
The same behavior could by achieved by [self dismissViewControllerAnimated:YES completion:nil] (before iOS 5 [self dismissModalViewControllerAnimated:YES]), as there's always at most one view controller presented (modally) at a time.
However, the point of the delegation pattern is that a single view controller could be presented in different ways such as modally or by being push to the navigation stack. That view controller doesn't know how it was presented (well, it could figure it out, but it should not care). The only thing it is supposed to do is to notify it's parent, i.e. the delegate, that its work is done. The delegate then decides how to remove the view controller (dismiss modal or pop from navigation stack etc.) or that the child should stay because the results of its work are insufficient. So the main idea is reusability of view controllers.
[[self presentingViewController] dismissViewControllerAnimated:YES completion:NULL];
That may be the simplest, but it's often not very useful.
Modal views typically need to return some information to their caller; that's why they're modal. In more traditional SDKs modal windows block their caller until the modal window is dismissed. The result of the modal window is then returned to the caller. E.g.:
int result = ShowModalDialog("Do you want to continue?");
if (result == kYes)
{
doSomething();
}
else
{
return;
}
In UIKit, -presentModalViewController: does not block, so you need some other mechanism for the modal view controller to return information to the presenting view controller. Typically that's done with delegation, though there are other ways (such as having the presenting controller handle the left and right UINavigationBar buttons).
If the modal view controller needs to return a value to its presenting view controller then that's done via delegation, and in that case it makes sense for the presenting controller to dismiss the modal controller after it has received the result. That's the original pattern.