In my app I have a drill-down type interface as follows:
My root view, which has a list of items and an "Add" button.
Selecting an item pushes the "Detail" view on the navigationController.
Selecting the "Add" button pushes an "Add" view.
How would I transition between the Add view to the Detail view?
I'm thinking of doing an unanimated "pop" on the Add view and push the Detail controller on, but how do I make the second part animated, so the detail view would either slide in vertically or fade in from the Add view?
Thanks,
Kelso
Based on Ramin's reply, you can try this way, which is used by Apple in several samples:
MyListViewController.h
#import "MyAddViewController.h"
#interface MyListViewController : UITableViewController <MyAddViewControllerDelegate> {
}
- (IBAction)add:(id)sender;
#end
MyListViewController.m
// Action for bring up add view controller
- (IBAction)add:(id)sender {
MyAddViewController *addViewController = [[MyAddViewController alloc] initWithStyle:UITableViewStyleGrouped];
addViewController.delegate = self;
UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:addViewController];
[self presentModalViewController:navigationController animated:YES];
[navigationController release];
[addViewController release];
}
// MyAddViewController's delegate method, dismiss the add view controller in here
- (void)addViewController:(MyAddViewController *)addViewController didAddData:(MyData *)data{
if (data) {
MyDetailViewController *detailViewController = [[MyDetailViewController alloc] initWithStyle:UITableViewStylePlain];
detailViewController.data = data;
[self.navigationController pushViewController:detailViewController animated:YES];
[detailViewController release];
}
[self dismissModalViewControllerAnimated:YES];
}
MyAddViewController.h
#protocol MyAddViewControllerDelegate;
#class MyData;
#interface MCCourseAddTableViewController : UITableViewController {
#private
MyData *data;
id <MCCourseAddDelegate> delegate;
}
// MyData could be NSManagedObject if you want to use Core Data
#property(nonatomic, retain) MyData *data;
#property(nonatomic, assign) id <MyAddViewControllerDelegate> delegate;
- (void)save;
- (void)cancel;
#end
#protocol MyAddViewControllerDelegate <NSObject>
#optional
- (void)addViewController:(MyAddViewController *)addViewController didAddData:(MyData *)data;;
#end
MyAddViewController.m
- (void)save {
if (self.delegate != nil) {
if ([self.delegate conformsToProtocol:#protocol(MyAddViewControllerDelegate)]){
if ([self.delegate respondsToSelector:#selector(addViewController:didAddData:)]){
// Send data back to List View, to bring up detail view controller and dismiss add view controller
[self.delegate addViewController:self didAddData:self.data];
}
}
}
}
- (void)cancel {
if (self.delegate != nil) {
if ([self.delegate conformsToProtocol:#protocol(MyAddViewControllerDelegate)]){
if ([self.delegate respondsToSelector:#selector(addViewController:didAddData:)]){
// Send nil back to ListView, to dismiss the add view controller only
[self.delegate addViewController:self didAddData:nil];
}
}
}
}
You could push the Add View onto the Details view and set a variable to remember that.
When the user try to pop the Add View, you check the previous variable and, if set, you pop directly to the root controller(that should pop the Details view automatically).
Marco
Make the add view a modal and in there provide a "Done" (or "Save") and a "Cancel" button. If the user hits cancel, you just close the modal. If they hit Done you save the new record to the table model, then do a table reload on the root view before returning. To be nice, you can flash the newly added item.
A clean way to set this up is to make the modal controller implement a delegate that expects a 'Done' protocol method and have the root controller implement it and set itself as the delegate.
This way, the root controller is notified when the user hits 'Done' so it can encapsulate all that needs to happen. If you want to go directly from add to detail view the delegate method can do a 'push' for the newly added record and you'll get a nice transition from modal sliding down to detail view.
Related
I have 2 ViewControllers, in 1st - TableView and in 2nd - button with label on it. When I click on the button in 2nd ViewController I need to go back on TableView and set in
cell.detailTextLabel.text
text from label on the button.
For go back to first view I use:
[self.navigationController popToViewController:[self.navigationController.viewControllers objectAtIndex:1] animated:YES];
but how I can set label from second view to:
cell.detailTextLabel.text
in first view?????
I would define a protocol & delegate in the second view controller
#protocol SecondViewController;
#interface SecondViewController : UIViewController
#property (nonatomic, assign) id<SecondViewController> delegate;
#end
#protocol SecondViewController <NSObject>
- (void)secondViewController:(SecondViewController *)controller didTappedOnButton:(UIButton *)button;
#end
then when the button is tapped call the delegate:
- (IBAction)buttonTapped:(UIButton *)sender
{
// do somthing..
// then tell the delegate about the button tapped
[self.delegate secondViewController:self didTappedOnButton:sender];
}
In your first view controller implement the protocol
#interface FirstViewController : UIViewController <SecondViewControllerDelegate>
when you push the second view controller, set the first as the second delegate:
- (void)someMethodThatPushTheSecondViewController
{
SecondViewController *svc = [[SecondViewController alloc] init];
[self.navigationController pushViewController:svc animated:YES];
svc.delegate = self;
}
And implement the delegate method to get notified when the button tapped
- (void)secondViewController:(SecondViewController *)controller didTappedOnButton:(UIButton *)button
{
// do somthing after button tapped
// you can get the button title from button.titleLabel.text
}
To access a parent class method or property you gotta implement a protocol, and use it's delegates. You can access child class methods/properties using it's class object that you create in the current (parent) class. But how you want to access a parent class entity from a child class? YES, implementing protocols.
Or the newbie way: after tapping your button, save the needed value into NSUserDefaults. Then, when you go to your parent class (viewController 1), ion viewWillAppear, check that saved value, and if it's not nil, display it.
[self.navigationController popViewControllerAnimated:YES];
I am having issues with the back button not showing up on the SettingsViewController. The navigation bar does show up when the view is pushed, but no back button.
I am creating this inside a view controller, which is not a navigation controller. Any ideas or suggestions on what is actually going on here.
- (void)viewDidLoad
{
self.title = #"Settings";
}
- (IBAction)showSettingsModal:(id)sender
{
SettingsViewController *settingsViewController = [[SettingsViewController alloc] initWithNibName:#"SettingsViewController" bundle:nil];
UINavigationController *navController = [[[UINavigationController alloc] initWithRootViewController:settingsViewController] autorelease];
[self presentModalViewController:navController animated:YES];
[settingsViewController release];
}
You are creating a new navigation stack. You will need to add your own Back button and set the action of that to a delegate method on the calling VC to dismiss it.
UPDATE:
There seems to be lots of confusion about where and how to dismiss ModalViewControllers. The wrong thing to do in most cases is to call the Dismiss method from the Modal VC itself if you are wanting the parent to act on that dismissal. Instead, use delegation. Here is a simple example:
ModalViewController.h:
#protocol ModalViewControllerDelegate
-(void)dismissMyModalVC;
#end
#interface ModalViewController : UIViewController {
id < ModalViewControllerDelegate > delegate;
}
#property (nonatomic, retain) id < ModalViewControllerDelegate > delegate;
// The rest of your class properties, methods here
ModalViewController.m
#synthesize delegate;
...
// Put in the Method you will be calling from that Back button you created
[delegate dismissMyModalVC];
CallingViewController.h:
#import "ModalViewController.h"
#interface CallingViewController : UIViewController
<ModalViewControllerDelegate>
// Rest of class here
CallingViewController.m:
ModalViewController *mvc = [[ModalViewController alloc] initWithNibName:#"ModalViewController" bundle:nil];
mvc.delegate = self
[self presentModalViewController:mvc animated:YES];
...
// The ModalViewController delegate method
-(void)dismissMyModalVC {
// Dismiss the ModalViewController that we instantiated earlier
[self dismissModalViewControllerAnimated:YES];
That way the VC gets dismissed properly from the controller that instantiated it. That delegate method can be modified to pass along objects as well (like when you are finished logging a user in, etc)
SettingsViewController does not have a back button because it is at the bottom of stack. If you want a button to dismiss the modal dialog, you will have to add it yourself.
you can try this
UIBarButtonItem * backButton = [[UIBarButtonItem alloc]initWithTitle:#"Back"style:UIBarButtonItemStylePlain target:self.navigationItem.backBarButtonItem action:#selector(dismissModalViewControllerAnimated:)];
You are presenting your new controller as modal view controller. Modal controller presents its topmost. You should:
[self.navigationController pushViewController:navController animated:YES];
to push view controller onto the stack, and then you will see Back button.
Read Apple documenation on presenting view controllers:
https://developer.apple.com/library/ios/#featuredarticles/ViewControllerPGforiPhoneOS/ModalViewControllers/ModalViewControllers.html
EDIT Didn't see that the calling view controller is not part of the navigation controller. In that case, you will have to create back button manually, and set it as a left bar navigation item.
So, I have a tabbarcontroller, and I pass a notification to dismissModalViewController when a particular tabBarItem is touched.
It is working well and the modal View Controller is dismissed. But I want to change it in a particular way, and it does not work as I expect it to...
I have the observer initialized before the notification is posted. These are the tabBarItems -
NSArray *viewControllerss = [[NSArray alloc] initWithObjects: myProfileDataViewController,
sampleViewController,reminderInfoViewController, nil];
[self.tabBarContr setViewControllers:viewControllerss animated:YES];
self.tabBarContr.selectedIndex = 2;
I send a notification on the viewWillAppear of sampleViewController and when I choose that tabBarIcon, it dismisses the TabBarController.
BUT I want the sampleViewController to be on the left most of the UITabBar.
And so I add it like
NSArray *viewControllerss = [[NSArray alloc] initWithObjects: sampleViewController,
myProfileDataViewController, reminderInfoViewController, nil];
THIS DOES NOT DISMISS TAB BAR CONTROLLER.
Note: Please see the order in which NSArray is initialized.
The notification is posted in the viewWillAppear ofsampleViewController` and observer in the respective view controller which presents the modal view controller
Could you put a NSLog right before you post the notification?
See if you get any output when the app loads.
EDIT: Adding onto the answer based on your response
In your sampleViewController could you try this:
Make it conform to the UITabBarControllerDelegate. Your sampleViewController class interface should be something like this:
#interface SampleViewController : UIViewController <UITabBarControllerDelegate>
Then in the .m of your sampleViewController, in the viewDidLoad, set the delegate to be the sampleViewController (self in this case)
-(void) viewDidLoad
{
[super viewDidLoad];
// Assuming you have a reference to your tabBarController somewhere
[self setDelegate:self]; // try this line or the line below
// [[self tabBarController] setDelegate:self];
// The rest of your drawing code here
}
Now implement the delegate method somewhere inside the sampleViewController .m file.
-(void)tabBarController:(UITabBarController *)tabBarController didSelectViewController:(UIViewController *)viewController
{
// I've included this to see if this method actually gets called or not.
NSLog(#"Dismissing modal view controller");
// check to make sure sampleViewController tab was pressed by checking
// the class type of the viewController parameter being passed in
if ([viewController isKindOfClass:[SampleViewController class]]
{
// I assume you have a pointer reference to that modal view controller
// you want to dismiss
[self dismissModalViewController:theUnwantedViewController animated:YES];
}
}
See if that works.
everywhere on the internet I find examples of how to add rows into a table view by having a special row "Add Row" with a green plus. But I don't want that.
I want to have a plus button in the titlebar of MyTableViewController, and invoke some Modal Add View Controller with a XIB file with just a single text field to fill it in. In this Modal Add View Controller I want to fill in this text field, and after I press Done, Modal Add View Controller dismisses, and I want to find that text added to a MyTableViewController table view.
I have a property in my MyTableViewController to hold all the lines of it:
#property (nonatomic, retain) NSMutableArray *list;;
I just can't get adding rows to work. I don't see where I could do the
[list addObject:];
Here's the code of the MyTableViewController addItem method which I invoke, when a user presses a plus button in the titlebar:
- (IBAction) addItem: (id) sender;
{
NSLog(#"Adding item...");
//Preparing "Add View" which has a single text field
AddViewController *addViewController = [[AddViewController alloc] init];
addViewController.title = #"Add Item";
UINavigationController *modalController = [[UINavigationController alloc]
initWithRootViewController:addViewController];
[addViewController release];
// Showing the prepared Add View controller modally
[self.navigationController presentModalViewController:modalController animated:YES]
NSLog(#"Modal controller has been presented.");
[modalController release];
}
And here is the code in the AddViewController, which is invoked after the textfield is typed in and pressing Done in the titlebar:
- (IBAction) done: (id) sender
{
NSLog(#"Reached Done");
if (textField != nil) {
self.fieldText = textField.text;
}
NSLog(#"About to dissmiss modal controller...");
[[self parentViewController] dismissModalViewControllerAnimated:YES];
NSLog(#"Modal controller has been dismissed.");
}
It is pretty common to create a delegate protocol for such Add Controller and make the parent controller its delegate.
When the Add Controller is "done" (i.e. not cancelled with a possible Cancel button), it calls a delegate method, say, addControllerIsDone: to let the parent table view controller know that it should take the set value, add it to the list, and dismiss the Add controller.
You could also pass the list to the Add Controller and let it add the set value itself before the [parentViewController dismissModalViewControllerAnimated:YES] call.
It depends whether you want to keep the control of the list in your table view controller or you want to pass it to the Add Controller.
And after the Add Controller is dismissed, you can either figure out where the cell for new entry should be added in the tableView and insert it with a nice animation, reload the section (animation also possible) or whole tableView (animation not possible).
First option could looke like this:
#class AddViewController;
#protocol AddViewControllerDelegate <NSObject>
- (void)controllerIsDone:(AddViewController *)controller;
#end
#interface AddViewController : UIViewController
#property (nonatomic, assign) id<AddViewControllerDelegate> delegate;
#end
And the 'done' code
- (IBAction) done: (id) sender
{
......
[self.delegate controllerIsDone:self];
NSLog(#"About to dissmiss modal controller...");
[[self parentViewController] dismissModalViewControllerAnimated:YES];
NSLog(#"Modal controller has been dismissed.");
}
And the MyViewController:
#interface MyViewController : UIViewController <AddViewControllerDelegate>
#end
So it has to implement the controllerIsDone: method. Like this for example:
- (void)controllerIsDone:(AddViewController *)controller
{
[self.list addObject:controller.textField.text];
}
As the AddViewController dismisses itself, MyViewController doesn't have to do it in the delegate method. But the good practice would be that if you popped up the modal view controller, you should also dismiss it, just for symmetry's sake. ;)
In this case, of course the textField has to be a publicly accessible property.
I'm sure you'll figure out the second option.
Read up on Decorator pattern in Cocoa Fundamentals Guide.
After you dismiss your modal controller:
[self addObjectToMyModel:newObject];
such that if you called [tableView reloadData] it would show up, but you don't need to call that, instead:
you need to know where the new object will appear in your table, determine the indexPath, and:
NSIndexPath *indexPathOfInsertedCell = …;
[self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:indexPathOfInsertCell]
withRowAnimation:UITableViewRowAnimationFade];
I have a mainViewController. I call [self pushModalViewController:someViewController] which makes someViewController the active view.
Now I want to call a function in mainViewController as someViewController disappears with [self dismissModalViewController].
viewDidAppear does not get called probably because the view was already there, just beneath the modal view. How does one go about calling a function in the mainViewController once the modalView dismisses itself?
Thanks a lot!
This answer was rewritten/expanded to explain the 3 most important approaches (#galambalazs)
1. Blocks
The simplest approach is using a callback block. This is good if you only have one listener (the parent view controller) interested in the dismissal. You may even pass some data with the event.
In MainViewController.m
SecondViewController* svc = [[SecondViewController alloc] init];
svc.didDismiss = ^(NSString *data) {
// this method gets called in MainVC when your SecondVC is dismissed
NSLog(#"Dismissed SecondViewController");
};
[self presentViewController:svc animated:YES completion:nil];
In SecondViewController.h
#interface MainViewController : UIViewController
#property (nonatomic, copy) void (^didDismiss)(NSString *data);
// ... other properties
#end
In SecondViewController.m
- (IBAction)close:(id)sender
{
[self dismissViewControllerAnimated:YES completion:nil];
if (self.didDismiss)
self.didDismiss(#"some extra data");
}
2. Delegation
Delegation is the recommended pattern by Apple:
Dismissing a Presented View Controller
If the presented view controller must return data to the presenting view controller, use the delegation design pattern to facilitate the transfer. Delegation makes it easier to reuse view controllers in different parts of your app. With delegation, the presented view controller stores a reference to a delegate object that implements methods from a formal protocol. As it gathers results, the presented view controller calls those methods on its delegate. In a typical implementation, the presenting view controller makes itself the delegate of its presented view controller.
MainViewController
In MainViewController.h
#interface MainViewController : UIViewController <SecondViewControllerDelegate>
- (void)didDismissViewController:(UIViewController*)vc;
// ... properties
#end
Somewhere in MainViewController.m (presenting)
SecondViewController* svc = [[SecondViewController alloc] init];
svc.delegate = self;
[self presentViewController:svc animated:YES completion:nil];
Somewhere else in MainViewController.m (being told about the dismissal)
- (void)didDismissViewController:(UIViewController*)vc
{
// this method gets called in MainVC when your SecondVC is dismissed
NSLog(#"Dismissed SecondViewController");
}
SecondViewController
In SecondViewController.h
#protocol SecondViewControllerDelegate <NSObject>
- (void)didDismissViewController:(UIViewController*)vc;
#end
#interface SecondViewController : UIViewController
#property (nonatomic, weak) id<SecondViewControllerDelegate> delegate;
// ... other properties
#end
Somewhere in SecondViewController.m
[self.delegate myActionFromViewController:self];
[self dismissViewControllerAnimated:YES completion:nil];
(note: the protocol with didDismissViewController: method could be reused throughout your app)
3. Notifications
Another solution is sending an NSNotification. This is a valid approach as well, it might be easier than delegation in case you only want to notify about the dismissal without passing much data. But it's main use case is when you want multiple listeners for the dismissal event (other than just the parent view controller).
But make sure to always remove yourself from NSNotificationCentre after you are done! Otherwise you risk of crashing by being called for notifications even after you are deallocated. [editor's note]
In MainViewController.m
- (IBAction)showSecondViewController:(id)sender
{
SecondViewController *secondVC = [[SecondViewController alloc] init];
[self presentViewController:secondVC animated:YES completion:nil];
// Set self to listen for the message "SecondViewControllerDismissed"
// and run a method when this message is detected
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:#selector(didDismissSecondViewController)
name:#"SecondViewControllerDismissed"
object:nil];
}
- (void)dealloc
{
// simply unsubscribe from *all* notifications upon being deallocated
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (void)didDismissSecondViewController
{
// this method gets called in MainVC when your SecondVC is dismissed
NSLog(#"Dismissed SecondViewController");
}
In SecondViewController.m
- (IBAction)close:(id)sender
{
[self dismissViewControllerAnimated:YES completion:nil];
// This sends a message through the NSNotificationCenter
// to any listeners for "SecondViewControllerDismissed"
[[NSNotificationCenter defaultCenter]
postNotificationName:#"SecondViewControllerDismissed"
object:nil userInfo:nil];
}
Hope this helps!
Using exit (unwind) segue
When you're using storyboards and segues you can use a very handy approach with minimal code to dismiss a modal view controller and inform the underlying view controller that the modal view controller has been closed.
Using exit (unwind) segues you gain 3 advantages:
you don't need to write any code to dismiss the modal view controller and
you can have iOS call a callback method inside the underlying view controller that has presented the model view controller.
you use the very same semantics that you already know from implementing prepareForSegue:
Implement it with only 2 steps
Create an action method in the Parent view controller that presents another (modal) view controller:
Swift
#IBAction func unwindFromSegue(segue: UIStoryboardSegue) {
print("Unwind from segue", segue.identifier)
}
Objective-C
- (IBAction)unwindFromSegue:(UIStoryboardSegue *)segue {
NSLog(#"Unwind from segue %s", segue.identifier);
}
In the storyboard, On child view controller right click the exit segue (aka unwind segue, it's the last of the icons at the top of your view controller), drag & drop unwindFromSegue: to your button and select action.
You're done! Now the modal view controller closes when you click the dismiss button and unwindFromSegue: informs your underlying view controller(Parent) that the modal view controller(Child) has closed.
Here's a callback solution which takes less modifications to your modal and parent:
In the Model's .h add:
#property (nonatomic, copy) void (^dismissed)();
In the Model's .m put this in the completion when you dismiss the modal:
[self dismissViewControllerAnimated:YES completion:^{
if(self.dismissed)
self.dismissed();
}];
In the parent view controller when you instantiate your modal set the dismissed callback:
Modal = //Init your modal
[Modal setDismissed:^{
//do stuff you wanted when it's dimissed
}];
[self presentViewController:Modal animated:YES completion:nil];