Very simple use case: Let's say an iOS app displays a MovieListController view (inside of a UINavigationController) with a list of movies. When the user touches on one, the app pushes a MovieDetailController onto the navigation stack (i.e. [[MovieDetailController alloc] initWithMovieId:(NSString *). In the MovieDetailController's viewDidAppear: method, it makes an HTTP call to retrieve details based on the movie ID passed into it.
The challenge is that the MovieDetailController gets pushed onto the navigation stack right away, and for a second or two while the details haven't been retrieved, the view shows a bunch of blank fields, which is undesirable.
To get around this, I'm thinking of having the MovieListController not push the MovieDetailController onto the stack right away. Instead, it would put up a progress indicator (I'm using SVProgressHUD), then call MovieDetailController's initWithMovieId: method which would kick off the HTTP call. Then when the data is received, the MovieDetailController would make a callback back to MovieListController to remove the progress indicator and then push the MovieDetailController onto the navigation stack.
Is there a better pattern for this type of scenario? Should I be considering having the MovieDetailController push itself onto the navigation stack when it's ready?
Note: I have considered loading the detail view and putting up an activity indicator, but you'll still be able to see an 'empty view' behind it which looks a bit weird. I have also considered just having the MovieListController retrieve the details itself but this seems to break the encapsulation model - the MovieListController should just be concerned about listing movies, not about their details.
Any thoughts? This Movie stuff is just an example - looking for a general pattern here.
Personally I would take the following approach.
User selects the movie they want details for
Push to the detail view and rather than showing your skeleton view with empty fields, overlay a loading view, you could continue using your progress HUD on top of this for any animations you gain with that.
Once the results come down, remove your HUD and the loading overlay view that is hiding all the data/fields
The reason I would go this route rather than showing the HUD before pushing the view controller is that you can give the user the opportunity to cancel their selection. I am not familiar with SVProgressHUD but hopefully when a HUD is displayed you can enable touches, specifically the user touching Back on your UINavigationController in the event they accidentally selected the movie or the request is just taking longer than they are willing to wait for.
It also separates the logic form your list view, your detail view is standalone and could be initialized anywhere in your app (maybe you want to cross link similar movies within a movie detail view) and you do not need to rewrite the logic of the presenting view waiting on the results to come back.
In this situation, I personally would return to the model-view-controller pattern.
There are several system apps that display a detail view from a list of objects, e.g. Calendar, Contacts, etc. Presumably, as with EKEvent and ABPerson in their respective apps, the main view controller maintains a list of the model objects. When the user selects one of the items, the main view controller passes the selected model object to the detail view controller. The detail view controller itself doesn't have to do any data loading. So, like #ChrisWagner said, we want to separate the logic from the view controller.
Method
Similarly, you might want to use a MovieList class that stores an array of Movie objects. Each Movie stores the values for all the fields in the detail view controller - essentially, all the information the app needs about the movie. For example, you might have a property NSString *movieTitle, or NSDate *premiereDate. The movieTitle would be set by the MovieList at initialization because it's just metadata; on the other hand, the premiereDate might be nil if the data hasn't loaded, so you would have a property BOOL isLoaded to check for this condition.
You could then proceed in one of two ways:
1) Say the main view controller wants to push a detail view controller for a movie. Then the main view controller would dig the appropriate Movie out of the MovieList and check if it's loaded. If not, it would call something like -(void)loadContents on the Movie. When the model object is finished loading, it will post a notification that it's finished loading. At this point the main view controller will dismiss its progress view and push the detail view. If you use (1), it's not as important to use a MovieList coordinator.
2) If you want to be more aggressive about loading the movie information, you could implement a method on MovieList that calls loadContents on its Movies in the background. Then there's a higher chance that a movie will already be loaded when the user selects it.
Edit: Note that if you decide to use a MovieList type object, only the main view controller should be allowed to access this list. In a way, the model structure I've described parallels the view controller structure. The detail view controller doesn't know about the list view controller, so it shouldn't know about the MovieList either.
Benefit
The benefit of using this Movie and MovieList structure is that the data model is completely separated from the view controllers. That way, the user is free to cancel, select, present and dismiss view controllers while the data is loading. (Imagine if the user pressed Back to dismiss the progress HUD, canceling any HTTP data it would have gathered. Then, if he decides to come back to that same movie, the new detail view controller has to start loading afresh.) Also, if you decide to replace view controller classes in the development process, you won't have to copy and paste a bunch of data-handling code.
I think you'll find that structuring your app in this way not only gives you and the user more freedom, it also makes your app efficient and open to later extensions. (Sorry to post so late, but I wanted to include MVC in this pattern-related discussion!)
I think a more advanced route would be to start the detail requests as the cells are being being shown on in the tableview/collection view. Then as the cells move offscreen, cancel the requests. You might be loading movie details unnecessarily, but I don't think this is a big deal, because you'd only be filling your db with movie details you might not need and coredata can handle that. This is assuming that your api requests are happening on a bg thread and they don't affect the main(UI) thread.
Related
This might sound like duplicate, but I need farther understanding and explanation about my Scenario please.
In my app, I have one InfoViewController, which represents information when called (pushed)from other Controller like, 1)HomeViewController, 2)FavouriteViewController and 3)DownloadViewController. All 3 held by UITabViewController
InfoViewController had about 10 buttons and corresponding actions. I use separate singleton to hold all info objects.
All 3 ViewController(Held by TabVC)--> Loads object to Singleton --> InfoViewController uses that to Present Detail Info
HomeViewController - Its for new 'Info' to present,which changes everyday
When Information is present,
user can mark it as a Favorite, which then Listed(In UITableView) on FavoriteViewController.
User can save it in phone for future reference, which will be listed(In UITableView) on DownloadViewController.
On selection of cell all 3 represents details using InfoViewController.
Now I want to present only one instance on InfoViewController to be visible to user. Not all in all 3 tab. Currently I am switching it back to Main screen with ViewDidDisappear method. Which works only when I add InfoViewController as child to main 3 VC. Not by push.
Now my problem is, i tried to use Appdelegate to initialized SharedInfo Object
sharedInfo = [InfoViewController alloc]init], but it goes to black screen. I have to initialize it as
[self.storyboard instantiateViewControllerWithIdentifier:#"InfoViewController"];
but this is not allowed in AppDelegate or making shared instance on InfoViewController itself.
How do I achieve Only one Instance present to user at a time??
I think rephrasing what you need might help. I don’t think you really want a shared instance of the UIViewController. What you want is a single source of the data which is displayed in one or more views and a method to update that data in all the views when the data changes.
Two possible solutions are:
NSNotification
Any view which contains the shared data subscribes to notifications when the data is changed. When data is updated via the singleton it sends a broadcast to anyone listening to update the view data.
View Lifecycle
In this second method the data in a particular view is only updated when the user brings up that view. Note in a tab bar interface that the viewDidLoad is called when the view is initially loaded. DO NOT update your data here if it changes. Instead you want to update the data in viewWillAppear as this is called every time you navigate to that view. In viewWillAppear you have code that checks the data and updates it if needed.
There are other ways to skin this cat, but either of the two above should work for you.
I've noticed when opening multiple instances of a view, my memory continues to climb with the more views that the user opens. If the user starts to hit back the memory usage drops with each view controller closing. However, depending on whatever the user is doing he can open 20+ view controllers, how can I manage the memory utilization? Keep in mind I need all those views loaded in the background so they can be quickly loaded when the user hits back
Heres how I'm creating each instance:
let storyboard = UIStoryboard(name: "Storyboard", bundle: nil)
let vc = storyboard.instantiateViewControllerWithIdentifier("FriendPage") as! FriendVC
self.navigationController!.pushViewController(vc, animated:false)
How can I manage the memory utilization?
A navigation stack keeps all the view controllers loaded in memory. That's integral to the way it works.
As Mr. Beardsley says, you can set up your view controllers to free their large data structures in your viewDidDisappear method (including setting image views to nil) and then reload them in viewWillAppear. If you make sure everything is cached to disk it should reload quickly.
To go beyond that, you'd need to forgo a navigation controller and create your own parent view controller that displayed a series of child view controllers. You could make the parent keep track of the navigation path the user followed and save state data for each view controller to disk, and then on the user pressing the back button, re-invoke the previous view controller and reconstitute it from it's saved state data. As long as everything is loaded from disk and not from the network you should be able to get near instant display of each screen when the user presses the back button.
This would require a fair amount of custom work on your part but shouldn't be that hard.
There are methods like 'transitionFromViewController:toViewController:duration:options:animations:completion:' that let you create custom transitions between child view controllers. You should be able to easily create whatever transition effect you want.
By saving a list of the view controllers that the user visited and a block of state data needed to recreate each view controller from disk you should be able to simulate a navigation stack while only having one child view controller active and in memory at a time.
Before going down this path, though, I would suggest looking at your user interface and seeing if there is a way to limit the depth to which the user can navigate. You could add some sort of limit to the depth of the user's navigation. The details would depend on your app design.
Unless you are running into memory pressure issues, I would not worry about it. How large does your memory usage grow when you navigate 20 levels deep in your view controllers? If you are running into issues, then you would have to save the state of the previous view controllers to persistent storage, and then set your current view controller as the root. When you go back, you'd have to reinstantiate the view controller and restore the state.
A middle of the road approach might be to have view controllers release any image, or other large binary data, when a new controller is added to the stack. When you navigated back, the view controller would have to reload the data from disk or the network.
my situation is:
My app need to display the views normally but when I press and call a view that will display some sensitive information, I need to be logged, so a login view need be displayed. The trick for me is: when I call presentViewController and load the view, the view is called in a modal way that hide the tab bar and I can`t access other views.
Other thing is I`m doing the check on if user is logged in viewDidAppear, is that a bad practice?
tks for any reply.
In such cases, the concept of container view controller is your friend. You have a UIViewController that encapsulates all the business logic, in your case, the login logic. It manages a view that, say, has a form-like structure where the user enters the data. You can add this as a child of the parent view controller (where your tabs exist). After he presses login (or something), you can remove this child view controller and continue where you left off!
No, checking in viewDidAppear for session validation is something that I do all the time, I think you are fine here.
Edit
In response to a comment. Yes, session validation viewDidAppear can cause problems. A careful application design is the key here. I personally store the login information of the user (as a user model) in NSUserDefaults and remove it whenever the user logs out.
I'm working on an iPad app that has two controllers, a Login Controller and a View Controller. The Login Controller challenges the user for a username/password and once authenticated, there's a modal segue to the View Controller.
I've implemented a timeout wherein after 20 minutes of inactivity, the app segues back to the Login Controller. However, when the user logs back into the app, the state of the View Controller isn't preserved.
Is there a way to pass the View Controller object back to the Login Controller for re-use after logging into the app again? Is there a better way to manage the state?
Two possibilities come to mind...
You can create a model object either as a "singleton" or possibly owned by the application delegate and update it from the view controller and read from it whenever your view controller's view will appear.
The other option would be to have the view controller as the app's root controller and the login controller a modal overlay.
Your comment "Manage the state" is the answer you seek.
If there are changeable things about your view controller you'd like to save, then save them as they change, (either in NSUserDefaults, or CoreData, or some other persistent store) and have them populate when ViewController calls viewDidLoad.
Storing an entire UIViewController at the AppDelegate level just to preserve a handful of values is likely to be very wasteful, and won't help you at all if the app terminates. For this and many other reasons, your best bet is to follow MVC and make your model a persistent store which feeds the view.
I am implementing a custom URL scheme which will add entities to my data model. The details for the entity are contained in the URL. The basic idea is that an email link (or a link from another app) will open my app and add the new entity.
The problem is, I can never be sure which state my app will be in when responding. Any number of view controllers might be in view. If the list of entities is in view, I need to insert a new row for that entity. If other views are on screen, I need to react differently. Some views might also be modal.
I would be satisfied with a simple pattern when this happens - abort whatever the user is currently doing, and pop to the root view controller. From here I will probably push to a controller where I will show the new entity being added.
I experimented with always dismissing any modal displayed and popping to the root, with the benefit of not needing to know what exactly is being displayed:
[(UINavigationController *)self.window.rootViewController dismissViewControllerAnimated:NO completion:nil];
[(UINavigationController *)self.window.rootViewController popToRootViewControllerAnimated:NO];
This works reasonably well, but there at least two cases where it is insufficient:
If some object was created when the modal was presented (the modal is then used to modify the new object), and it is the delegate's responsibility to delete the object if the user cancels, the entity will remain alive.
If a UIActionSheet is being displayed, all bets are off. I can't dismiss this without knowing the controller that displayed it, having access to that controller, and sending it a message to dismiss the action sheet. Without doing so, the root view controller is popped to but the action sheet stays on screen. Subsequent taps on the action sheet of course cause a crash, since the controller that displayed it is gone.
How might I handle this robustly? Should I be trying to find out specifically which view controller is currently presented, and handling each scenario in turn? Or is there a more scalable solution that won't need updating each time I add controllers or change my application's flow?
It sounds like you are trying to do several things:
When the user clicks on your custom url, you want to add an "entity" to your model.
You want to display this new entity in some sort of EntityListViewController, which may or may not be on the ViewController stack.
You (may) want to pop off all view controllers above the EntityListViewController.
You want the user to know there was a new entity added (perhaps just by doing item 2).
You want to push some kind of EntityViewController, or if there is currently an EntityViewController in the view controller stack, you want to reload with the new entity's data.
It sounds like you have item 1 ready to go, since you didn't explicitly ask about handling the url click and inserting the new model object.
For the rest, a flexible and MVC-ish pattern would be to use NSNotificationCenter.
The code that inserts the new model object would "post" a notification:
[[NSNotifcationCenter defaultCenter] postNotificationName:#"entity_added" object:myNewEntity];
Then your various UI elements (e.g., UIAlertView and UIViewController subclasses) would listen for this notification and take some useful action (like closing themselves, or in the case of EntityListViewController or EntityViewController, reloading themselves).
For example, a UIViewController subclass might do this:
-(void) viewDidLoad
{
[super viewDidLoad];
[[NSNoticationCenter defaultCenter] addObserver:self selector:#selector(onNewEntity:) name:#"entity_added" object:nil];
-(void) onNewEntity:(MyEntity*)entity
{
// close, or redraw or...
}
-(void) dealloc
{
[[NSNoticationCenter defaultCenter] removeObserver:self];
// if not using ARC, also call [super dealloc];
}
To keep your life simple (and not worry too much about all the different UI states), I would consider doing this when the notification occurs:
Have the EntityListViewController redraw itself (does not matter if there something on top of it).
Show some sort of short-lived indicator in the nav bar (or somewhere else you know is always visible), or play a sound so the user knows that an entity was added.
And that's all.
If you take this approach, then there is minimal impact on whatever the user is/was doing, but when they do navigate back to the EntityListViewController it has all the new entities already displayed.
Clearly, if the click on the custom URL could possibly delete an existing entity, then it would be more important to pop off any viewcontrollers related to that entity. But this is also something you could do using the same pattern -- have the model or controller post the notification, and then have the various UI elements listen for it and take appropriate action.