I am currently loading a new view dynamically by evaluating the segue.identifier in the prepareForSegue:
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if([segue.identifier isEqualToString: #"DetailViewSegue"]) {
DetailViewController *detailController = segue.destinationViewController;
// ... do something with the controller
}
}
Some of those views need a bit of time because gathering the data from the Internet and displaying that in a table scheme needs a rather long time.
Therefore I want to display a Progress HUD like KVNProgress, however the main issue now is that the Progress bar shows up too late, right before the new view is ready. As far as I have seen is the main Problem, that the prepareForSegue method and therefore the KVNProgress was called immediately before loading all the data, but loading a new view seems to be preferred instead.
Another thing I tried was to call KVNProgress within a IBAction or didSelectRowAtIndexPath and to call the performSegueWithIdentifier within his own thread. However this is (as the console output suggests) discouraged, and does not really work either.
Thanks!
EDIT:
There were multiple attempts achieving this, currently I am trying to do it that way:
(IBAction)buttonPushed:(id)sender {
[KVNProgress showWithStatus:#"Loading"];
[self performSegueWithIdentifier:#"DetailViewSegue" sender:sender];
}
As I've described, i think this is the earliest I can display the ProgressHUD before the new action is loaded. However it seems that the current view is blocked while the new one is loaded, and therefore the hud is not shown.
There are two solutions for this.
When the user taps on a "Load data" button,
you display the HUD (and stay on the current screen)
load the datafrom the internet (typically on a background thread)
when it's complete, you call performSegueWithIdentifier to move to the DetailView screen
Alternatively, you might prefer to do this:
you call performSegueWithIdentifier as soon as the user taps the
button
you move to the new DetailView screen
in the DetailViewController.m, you display the HUD and call the
background thread to load the internet data
There's pros and cons to both (you might find the latter is more maintainable, as other screens call this DetailView screen and also need to load the internet data to populate this screen).
The key is to decide which way you want to implement this, then get that particular ViewController to do both parts, handling the HUD and loading the internet data.
Related
I am building an app that presents UITableView to the user, from which the user needs to make a selection. Once the selects a row, they are presented another ViewController which displays details of the selection they made on the previous ViewController.
Here's the catch: After the user makes the selection, the app needs to make a call to the network to retrieve some data to be displayed on the next ViewController. I was planning on calling a method from the prepareForSegue method which would return the results from the network call, and then call the appropriate ViewController, but I'm wondering if this is something that should be called from the "didSelectRowAtIndexPath" method (which I have not implemented).
My fear is that the second ViewController will be called BEFORE the call to the network returns with the data that I need to display. Is this even the place to put such a network call, or should I make this call from the "viewDidLoad" method of the destination ViewController instead? What is the best architecture and why?
Call prepareForSegue method inside didSelectRowAtIndexPath method to navigate to secondViewController then to display details, make network call inside viewDidLoad method of secondViewController and show activityIndicatorView until the data is fetched.
I would use some asynchronous loading starting in prepareForSegue and managed in the details view. Once the details view appears, you have to inform user that something is loading (an appropriate turning wheel exists for it), and then populate the interface after the loading has been done (or manage loading error).
But using viewDidLoad of the details view would be ok too, provided you always inform user of the current loading...
Don't use didSelectRow because it is just intended for selection... There can be no navigation on a tableview cell!
Asynchronous is preferred because it does not block user in some weird state waiting something he does really know.
The Background
I have an iOS application I am building with StoryBoard for the UI and AFNetworking for the back end. On my UIViewController I have some text boxes. When the user hits a submit UIButton I am making the AFNetworking call to a web service which returns an XML file.
The issue
I used the StoryBoard interface to hook up the button to a UITableViewController. Initially this worked fine because I had the code for the web service call inside the UITableViewController. The problem is that if the web service returns nothing I get a blank UITableView. So I moved the web service call into the UIViewController to be called when the button is pushed. OOPS! I make the web service call and the prepareForSegue gets called before the web service returns the data since its Asynchronous.
My Question
What is the best approach when getting data from a web service when you are using storyboards. Should I make the call inside the UITableViewController and then go back to the UIViewController if there is no data or an error returned from the service. If so how do I get back. I have tried self.navigationController popViewController but it comes up with a warning in the console that removing a view controller can mess up the stack.
Is there a way that I can control when the segue is called and only do it once I have data back from the web service?
Posting this step by step answer to help anyone else out that may need help with this:
Connect the two ViewControllers in Storyboard which will create a segue.
You simply select the first view controller (make sure the viewController is highlighted with a blue line and you didn't select like an image view or navigationItem. Then CTRL and click on the destination view controller (where you want to transition to).
The round circle in the middle is called the segue. If you click on that and go to your attributes panel on the right, you will see this:
In the identifier enter a name you want to call this transition: for example myNewPage.
Whenever you are ready to make the call to the next scene. Use this delegate method to do pass data from one controller to another or do anything special:
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
NameOfDestinationController *dataViewController;
dataViewController = segue.destinationViewController;
if ([segue.identifier isEqualToString:#"nameOfYourSegueIdentifier"]) {
dataViewController.variable1 = self.variable1;
...
}
}
Then in my case I needed to trigger the segue after I got a success from the AFNetworking web service call. So in the Success block I added this line:
[self performSegueWithIdentifier:#"nameOfYourSegueIdentifier" sender:self];
It works perfectly now!!!
I try to explain my problem. In appdelegate I have to select a rootViewController depending on the result of an asynchronous request (I'm using AFNetworking framework). In fact, I need to know if my user is profiled or not: if he is profiled I can show him app's Home, if he is not I have to show him a profilation view.
In storyboard I set the Home view as the designated entry point, but in this way this view is always shown until the asynchronous request is completed. Is there a way to make appdelegate wait for the response?
I think there is't good solution to let app delegate wait for the response because if the network connection will be poor the app loading time will be very long and OS could kill your app or user can turn it off.
You can add some loading view controller (with animation so user will know that the app is doing something) instead of home one and when you receive the response present appropriate view to the user (modal segue could do the job).
Hope this help
A better solution is to use splash screens. That is when your app gets loaded in AppDelegate, create and push a splash view controller. Which would just contain a single UIImageView covering whole screen showing your application splash image. Upon asynchronous call completion, pop that splash view controller and push your required view Controller.
Many apps use this way to download necessary asynchronous data before showing the app. So that user don't see empty screens or garbage data.
If something gets failed like internet connectivity failure or server response error, etc., Show error to user and perform error handling according to your app logic.
You can programatically navigate to the root view controller as
[self.navigationController popToRootViewControllerAnimated:YES];
This code can be put in the condition of result.
Or in your way, I think you are created a segue for navigation to the rootViewController. You can programatically perform a segue using
- (void)performSegueWithIdentifier:(NSString *)identifier sender:(id)sender
If you are using the AFNetworking, just add a method in the success block and pass the response to that method in a parameter of dictionary. Check your response in the method and choose the controller which you want to make make the root view controller from that method.
When using a UINavigationController, when the user is "diving in deeper" (pushing yet another controller on the stack), I have an opportunity to handle that in
-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
But how do I handle the opposite? When the user presses the back button, and a controller becomes the top controller again, I'd like it to potentially update some state because the controllers on the stack may have changed some things I want to reflect in the now visible controller.
Or, by analog, when I use modal segues to present new controllers, I get to pick a method that is called as an unwind segue when the presented controller exits. How can I do the same with navigation stack managed controllers?
(feel free to put a better title on this)
It turns out that you can disambiguate based on the response to isMovingToParentViewController. If it is YES your controller has just been placed topmost on the stack. If it is NO, your controller is returning to topmost, another push on top of it being popped. Example:
-(void)viewWillAppear:(BOOL)animated{
if (self.isMovingToParentViewController == NO) { // returning from even higher controller
[self updateForChangesThatMayHaveHappenedInSubController];
}
[super viewWillAppear:animated];
}
You can use the viewWillAppear: method to update the ui before the view becomes visible. If you want to pass data back up the chain, you should assign yourself as the delegate to your child and call an update function on the delegate before popping.
To have many clients (viewControllers in this case) update their views in response to a change of some shared data, you should use NSNotifications, or you should have the viewControllers observe certain values on the shared data-object (KVO).
ViewController should be as autistic as possible, meaning that they know all about the interface of downstream viewControllers, but have absolutely no idea about what viewController is upstream (talking back to an upstream viewController is usually done through delegation, and only to signal events that might indicate some change in viewController hierarchy, not in shared data state).
Checkout out the stanford lectures by Paul Hegarty, he explains this much better then I can.
I have an iOS app running right now in a storyboard with 3 viewcontrollers. The first one (initial view) features a play button to start a music stream and image for album cover of currently playing song. This scene has a a navigation controller and a bar button on it that will lead the user to the next view...
A list view populated with hard coded stream's that the user can choose from. Very simple and working fine still.
After choosing one, the user goes to a preview page that tells them about the stream before it begins to play. Still working like a charm until they want to continue from here.
If they user selects the stream from the preview page, the app "should" return the user to the initial ViewController and swap out the playing stream for the one selected. At first, I was mistakenly creating a new instance of the initial viewController, and after fixing that mistake, now have a few more questions someone might be able to help me with.
here is the IBAction for the button to select the stream:
- (IBAction)returnHome:(id)sender
{
[[self navigationController] popToRootViewControllerAnimated:YES]; // goes back to first view on the stack
}
Before finding this logic, I was using the prepareForSegue and setting the stream value of the destination to be what was selected. I was also trying to save the state of the first view but was unsure how to pass it down the line (or retain it) since I am moving through 3 ViewControllers and using a modal segue so they can go back if they choose to not pick a new stream.
Any advice will help, but please refrain from simply posting a link to the references. I have been there for 3 days straight and they do not speak a very beginner friendly lingo in the iOS reference docs.
You want to be able to inform Your first VC that user changed track?
You can simply use NSNotificationCenter.
First You have to "tune in", i.e. in viewDidLoad, Your view controller to listen to particular notifications:
- (void)viewDidLoad
{
[super viewDidLoad];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(changeStream:) name:#"UserWantToChangeStream" object:nil];
}
and implement method:
- (void)changeStream:(NSNotification *)notification
{
NSString *newStreamName = notification.object;
/* Change the stream code */
}
Don't forget to stop listening to the notifications:
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
Then You post notification after user has performed an action:
[[NSNotificationCenter defaultCenter] postNotificationName:#"UserWantToChangeStream" object:#"new_stream_name"];
In the example I pass NSString new_stream_name but You can pass any object.
Your first view controller will be informed.
Have you considered the usage of unwinding segues? they are a little bit tricky to understand and use, but they surely can help you out.
First of all, you have to create an IBAction on your FIRST viewcontroller (== the view controller where you want to "land" and pass your list selection to) that takes a UIStoryboardSegue as single parameter and leave the implementation empty. For example
- (IBAction) returnToHome:(UIStoryboardSegue*) segue{;}
Then, in your preview page view controller (in storyboard), drag a segue from your button (the one which is triggerng the IBAction that pops the navigation controller) to the little exit symbol in the lower right side of the view controller. A menu should pop out asking for returnToHome method. Delete from the button the previous IBAction as well (the one you called -(IBAction)returnHome:(id)sender)
In this way you should be able to do the same thing as before (popping back to the root view controller) without solving the problem BUT! if you implement an override of prepareForSegue:sender in your last view controller you'll have a reference to the root view controller where you can do whatever you need.
- (void) prepareForSegue:(UIStoryboardSegue*) segue sender:(id)sender
{
ViewController *vc = (ViewController *)segue.destinationViewController;
[vc.audioplayer pause];
vc.stream = self.stream;
}
This is a cleaner way to achieve the same result, since you're not making assumpions on your viewcontrollers hierarchy (what if tomorrow you'll add another view controller BEFORE the first one? The app will surely crash).
By the way, if you're into Storyboard/Segue business, check out my library which really simplifies storyboard work when it comes to "passing parameters": https://github.com/stefanomondino/SMQuickSegue (you can install it via cocoapods with pod 'SMQuickSegue')
I have figured out at least how to get it to work. I had a second navigation controller in the storyboard and after deleting that it successfully went back 2 views instead of just one.
Here is my logic for retaining the stream data and bringing it back to the initial view controller if anyone is curious.
- (IBAction)returnHome:(id)sender
{
ViewController *vc = (ViewController *)[self.navigationController.viewControllers objectAtIndex:0];
[vc.audioplayer pause];
vc.stream = self.stream;
[self.navigationController popToRootViewControllerAnimated:YES];
}
since I now only have one Navigation Controller, the index for the initial will be 0. I can grab the initial view and put a reference to it in vc.
Then I simply access the audioplayer from it, and if it is still playing, stop it and load up the scene.
Then in the initial ViewController logic, I have the viewDidAppear method to load the new stream selection into the display labels and images.
- (void)viewDidAppear:(BOOL)animated
{
// If there is no stream selected (first run)
// set the stream to RAPstation default
if (self.stream == nil)
{
self.stream = [[Stream alloc]initWithName:#"RAPstation" ...];
}
self.snameLabel.text = self.stream.sName;
self.sdescLabel.text = self.stream.sDesc;
}
Not sure if this is the right but it definitely works for now. If anyone can help make it cleaner please feel free.