UIPopoverController: update view after it is dismissed - ios

On iPad simulator, I have a ViewController A that presents an UIPopoverController whose contentViewController is ViewController B, inside which I have a button to dismiss the UIPopoverController.
When it is dismissed, I need to update the view of ViewController A based on some field in ViewController B.
In order to do this, I am declaring ViewController A as a property (weakref) of ViewController B so that within ViewController B where it dismisses the popover, I can say:
[self.viewControllerA.popover dismissPopoverAnimated:YES];
self.viewControllerA.popover = nil;
self.viewControllerA.textLabel.text = self.someField
Is this the correct way of doing it? Since there is no callback when we dismiss the popover pragmatically, I can't think of any better solution.
Anybody has a better idea? Passing view controllers around just seems awkward to me.

The best way is use of Delegation, just declare the delegate in your controller B like
#protocol ControllerSDelegate <NSObject>
-(void) hidePopoverDelegateMethod;
#end
and call this on action for passing the data and dismiss of controller like
if (_delegate != nil) {
[_delegate hidePopoverDelegateMethod];
}
and
in your controller A you can handle this delegate call
-(void) hidePopoverDelegateMethod {
[self.paymentPopover dismissPopoverAnimated:YES];
if (self.paymentPopover) {
self.paymentPopover = nil;
}
[self initializeData];
}

I think, delegates or sending NSNotification will make better.
Note:
A change of the execution sequence will do more perfection to your current code.
self.viewControllerA.textLabel.text = self.someField
[self.viewControllerA.popover dismissPopoverAnimated:YES];
self.viewControllerA.popover = nil;

Related

Navigate to RootViewController from ModalViewController

I have an app with 3 view controllers. A is the root, B is next and C is a modal view that appears / transitions when a button on B is clicked. What I want to do is sometimes move from C to A directly when a button on C is clicked (other times, the C->B transition should happen, which is ok). I've tried various options and incorporated info from different SO posts but in the end, stuck at the scenario where the direct transition from C to A happens but does show B momentarily.
Is there a way I can remove this momentary B appearance altogether. Also, I do need to have animations when the user goes from B to C or from C to B. It is the C to A transition that need not have an animation if there is a limitation there.
Hopefully you can chip in... have copied relevant code and further details below.
ViewControllerC.h:
#protocol ViewControllerCDelegate <NSObject>
- (void)didNavigateBackFromViewC:(ViewControllerC *)viewControllerC;
#end
#interface ViewControllerC : UIViewController
#property (nonatomic, weak) id < ViewControllerCDelegate > viewCDelegate;
#end
ViewControllerC.m:
- (IBAction)navigateToViewA:(id)sender
{
[self.viewCDelegate didNavigateBackFromViewC:self];
}
For the B->C and C->B transitions, I've used a separate transitioning controller that is a delegate of ViewControllerB. Also, ViewControllerB is a delegate of UIViewControllerTransitioningDelegate... I've not included those details for the sake of simplicity here but hopefully am able to convey the problem here about the C->A transition.
ViewControllerB.h:
#interface ViewControllerB () <ViewControllerCDelegate>
#end
ViewControllerB.m:
- (void)didNavigateBackFromViewC:(ViewControllerC *)viewControllerC
{
__block ViewControllerB *me = self;
[viewControllerC dismissViewControllerAnimated:YES completion:^{
[me.navigationController popToRootViewControllerAnimated:YES];
}];
}
really interesting question #vikram17000. First of all, you might want to try unwind segues, it seems that they can do what you want but I haven't tested that. It appered you were going the programmatic route, and here's what i found. I split (what I called) the 'back' and 'home' actions into two separate actions. Here's my ViewControllerC code:
-(IBAction)goBack:(id)sender
{
[self dismissViewControllerAnimated:YES completion:nil];
}
-(IBAction)goHome:(id)sender
{
[self.delegate ViewControllerCWillGoHome:self];
[self dismissViewControllerAnimated:YES completion:nil];
}
ViewControllerB is the delegate that gets the -...WillGoHome, and it popsToRootVC before VC C dismisses. This gets the behavior we want, C animates out and A is left - yay! Unfortunately there's a catch -
Unbalanced calls to begin/end appearance transitions for ViewControllerB: 0x7fc3c857ca80.
So is it possible? Yes. Does the os like it? No it does not. Hopefully this helps you get going though.
I've created sample project for your question.
There is protocol GoBackProtocol which contains method
-(void)controller:(UIViewController *)controller didPressBackButton:(UIButton *)button;
A and B controllers adopt GoBackProtocol. B controller implements -controller:didPressBackButton: method and decides based on button parameter what to do: hide presented controller with animation or hide it without animation and forward call to his delegate, which is controller A.
- (void)controller:(UIViewController *)controller didPressBackButton:(UIButton *)button
{
ViewControllerC *modalController = (ViewControllerC *)controller;
if (button == modalController.buttonB)
{
[controller.presentingViewController dismissViewControllerAnimated:YES completion:nil];
}
else if (button == modalController.buttonA)
{
[controller.presentingViewController dismissViewControllerAnimated:NO completion:nil];
[self.delegate controller:self didPressBackButton:nil];
}
}
Controller A implements -controller:didPressBackButton: as well and pops viewController B.
- (void)controller:(UIViewController *)controller didPressBackButton:(UIButton *)button
{
[self.navigationController popViewControllerAnimated:YES];
}
All Together it makes desired effect.
Please forget all custom protocol solutions here. Apple says:
The presenting view controller is responsible for dismissing the view controller it presented.
IMO you have at least two options here:
1) You could add the close button on C to public interface like
#property (nonatomic, strong, readonly) UIButton *closeButton;
Set the target differently and use a combination of dismissViewControllerAnimated:completion: and popToRootViewControllerAnimated: as in your method didNavigateBackFromViewC
2) You could use self.navigationController.presentingViewController in C to get access to the non-modal viewcontrollers.
As non-official option you could set the viewcontrollers array on a UINavigationController instance directly. Then you will get a Push Animation from C to A.
I haven't fully testing this out yet but results so far seem encouraging. Given that I wanted to be able to transition to B or C from A, and that B and C could navigate between each other, I've started playing around with making ViewControllerA as the container view controller for ViewControllerB and ViewControllerC. That allows me easy access to B / C from A, as well as the ability to transition between B and C seamlessly using custom transitions.

How to have an action in one view controller start another in a different view controller?

New to iOS development & Objective-C and am a little unsure how to go about solving this issue.
I'm working on a project that works like a video player. There are two ViewControllers:
MenuViewController (has a list of titles that act as buttons)
PlayerViewController (view where video plays)
In the MenuViewController, I want to be able to click onto a button (video title) :
- (IBAction)videoOne:(id)sender
{
PlayerViewController * vc = [[PlayerViewController alloc] initWithNibName:#"PlayerViewController" bundle:nil];
[self presentModalViewController:vc animated:YES];
}
and have an action that's currently defined in the PlayerViewController automatically execute as soon as its loaded.
Is there a way to have a button in one ViewController call the action in another ViewController as soon as that second ViewController has loaded?
Thanks!
The right way to solve this problem will be set up a delegate pattern between the two view-controllers.
Example:
#protocol PlayerViewControllerDelegate<NSObject>
{
-(void)playerViewControllerViewDidLoad:(PlayerViewController *)playerVC;
}
Then, in PlayerViewController.h create a weak delegate variable:
#property (nonatomic, weak) id<PlayerViewControllerDelegate> delegate;
In PlayerViewController.m, notify the delegate on viewDidLoad:
-(void)viewDidLoad {
[super viewDidLoad];
[self.delegate playerViewControllerViewDidLoad:self]
}
In MenuViewController:
- (IBAction)videoOne:(id)sender
{
PlayerViewController * vc = [[PlayerViewController alloc] initWithNibName:#"PlayerViewController" bundle:nil];
vc.delegate = self
[self presentViewController:vc animated:YES];
}
Finally, implement the protocol in MenuViewController, and you're ready to go:
#interface MenuViewController : UIViewController<PlayerViewControllerDelegate>
#end
#implementation MenuViewController
-(void)playerViewControllerViewDidLoad:(PlayerViewController*)playerVC
{
[playerVC playVideo];
}
#end
It is usually not good practice to have one controller controlling the behavior of another. Instead, you can have the MenuViewController create the PlayerViewController and set variables so the new player knows how to behave based on its internal state.
There are several UIViewController methods that you can override in order to perform actions during the controller's lifecycle. Based on your question it seems like you want the viewDidLoad method.
I am not sure how you are passing videos between controllers, but if you were using URLs (to Youtube videos for example) then you could do something like the following:
// MenuViewController.m
- (IBAction)videoOne:(id)sender {
PlayerViewController* vc = [[PlayerViewController alloc] initWithNibName:#"PlayerViewController" bundle:nil];
// Pass any necessary data to the controller before displaying it
vc.videoURL = [self getURLForSender:sender];
[self presentViewController:vc animated:YES completion:nil];
}
// PlayerViewController.m
- (void)viewDidAppear {
// View did appear will only be called after the controller has displayed
// its primary view as well as any views defined in your storyboard or
// xib. You can safely assume that your views are visible at this point.
[super viewDidAppear];
if (self.videoURL) {
[self playVideo];
}
}
You would need to define the property videoURL on PlayerViewController and expose it publicly. If you are using local files (such as from the user's photo storage) you could pass the video to the new view controller before presenting it.
There are other UIViewController lifecycle methods that you can override. They are explained in more depth in this post as well as Apple's UIViewController Documentation.
Edit: changed presentModalViewController:animated: to presentViewController:animated:completion: and changed viewDidLoad to viewDidAppear as it seems more appropriate for the question.
presentModalViewController:animated: is deprecated. Use presentViewController:animated:completion: instead.
In the completion Block, you can call a method on the presented view controller:
[self presentViewController:otherVC
animated:YES
completion:^{ [otherVC startPlaying]; }];
The completion is run after the presented controller's viewDidAppear.

Reloading data for collectionview

I have 2 ViewControllers
ViewControllerWithCollectionView (FIRST) and ModalViewControllerToEditCellContent (SECOND)
I segue from FIRST to SECOND modally. Edit cell. Return.
After dismissing SECOND controller, edited cell doesn't get updated until i call
[collection reloadData]; somewhere manually.
Tried to put it in viewWillAppear:animated:, when i check log, it's not called (after dismissing SECOND)
I've tried various solutions, but i can't brake thru (maybe I'm just too exhausted). I sense that I'm missing something basic.
EDIT dismiss button
- (IBAction)modalViewControllerDismiss
{
self.sticker.text = self.text.text; //using textFields text
self.sticker.title = self.titleText.text;// title
//tried this also
CBSStickerViewController *pvc = (CBSStickerViewController *)self.stickerViewController;
//tried passing reference of **FIRST** controller
[pvc.cv reloadData];//called reloadData
//nothing
[self dismissViewControllerAnimated:YES completion:^{}];
}
It's tough to tell from the posted code what's wrong with the pointer to the first view controller that you passed to the second. You should also be able to refer in the second view controller to self.presentingViewController. Either way, the prettier design is to find a way for the first view controller to learn that a change has been made and update it's own views.
There are a couple approaches, but I'll suggest the delegate pattern here. The second view controller can be setup to have the first view controller do work for it, namely reload a table view. Here's how it looks in almost-code:
// SecondVc.h
#protocol SecondVcDelegate;
#interface SecondVC : UIViewController
#property(weak, nonatomic) id<SecondVcDelegate>delegate; // this will be an instance of the first vc
// other properties
#end
#protocol SecondVcDelegate <NSObject>
- (void)secondVcDidChangeTheSticker:(SecondVc *)vc;
#end
Now the second vc uses this to ask the first vc to do work for it, but the second vc remains pretty dumb about the details of the first vc's implementation. We don't refer to the first vc's UITableView here, or any of it's views, and we don't tell any tables to reload.
// SecondVc.m
- (IBAction)modalViewControllerDismiss {
self.sticker.text = self.text.text; //using textFields text
self.sticker.title = self.titleText.text;// title
[self.delegate secondVcDidChangeTheSticker:self];
[self dismissViewControllerAnimated:YES completion:^{}];
}
All that must be done now is for the first vc to do what it must to be a delegate:
// FirstVc.h
#import "SecondVc.h"
#interface FirstVc :UIViewController <SecondVcDelegate> // declare itself a delegate
// etc.
// FirstVc.m
// wherever you decide to present the second vc
- (void)presentSecondVc {
SecondVc *secondVc = // however you do this now, maybe get it from storyboard?
vc.delegate = self; // that's the back pointer you were trying to achieve
[self presentViewController:secondVc animated:YES completion:nil];
}
Finally, the punch line. Implement the delegate method. Here you do the work that second vc wants by reloading the table view
- (void) secondVcDidChangeTheSticker:(SecondVc *)vc {
[self.tableView reloadData]; // i think you might call this "cv", which isn't a terrific name if it's a table view
}

Can i know in viewWillAppear that it was called after navigationController pop (back button)?

Say I have UIViewController A and B.
User navigates from A to B with a push segue.
Than user presses back button and comes to A.
Now viewWillAppear of A is called. Can I know in the code here that I came from back button (navigationController popTo...) and not by another way? And without writing special code in the B view controller.
hm, maybe you can use self.isMovingToParentViewController in viewWillAppear, see docs, if it is NO then it means the current view controller is already on the navigation stack.
I like to do the following in view controller A:
- (void) viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
if (_popping) {
_popping = false;
NSLog(#"BECAUSE OF POPPING");
} else {
NSLog(#"APPEARING ANOTHER WAY");
}
//keep stack size updated
_stackSize = self.navigationController.viewControllers.count;
....
}
- (void) viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
_popping = self.navigationController.viewControllers.count > _stackSize;
....
}
What you are doing is keeping track of whether your view controller (A) is disappearing because a view controller (B) is being pushed or for another reason. Then (if you did not modify the child view controller order) it should accurately tell you if (A) is appearing because of a pop on the navigation controller.
Add a BOOL property to UIViewController A:
#property (nonatomic) BOOL alreadyAppeared;
Then in your viewWillAppear: method, add:
if (!self.alreadyAppeared) {
self.alreadyAppeared = YES;
// Do here the stuff you wanted to do on first appear
}

Update tableView data

I have a UITableView embedded in a UIView that I can't figure out how to update. When I come to this page with the UITableView embedded in the UIView there's a button that when pressed brings up a modalForm. There is a textField that the user enters a name into and then presses another button to "Create" the object, dismiss the modalForm, and update the app data. However, I'm not sure how to refresh my UITableView data… Is there a way to tell the UITableView to refresh when the modalForm view is dismissed?
EDIT: I know i need to send this message [tableView reloadData]; in order to refresh, but I'm wondering where I can put it so it gets called upon dismissal of the modalForm?
In the view controller that is presenting and dismissing the modal, you should be calling the dismissViewControllerAnimated:completion method to dismiss the modal. The modal should not dismiss itself. You can use a completion block to execute whatever code you would like that will execute when the modal is finished dismissing. Example below.
[self dismissViewControllerAnimated:YES completion:^{
//this code here will execute when modal is done being dismissed
[_tableView reloadData];
}];
Of course don't forget to avoid capturing self strongly in the block.
If you end up having the modal dismiss itself, you will need either a delegate method so the modal can communicate back to the presenting view controller, or a notification sent by the modal and captured by the presenting view controller, or you could implement viewWillAppear: in the presenting view controller. This method will fire everytime the view is about to appear. Which means the very first time as well as after a modal is dismissed and is about to show the view it was presenting over top of.
----------------------------------------------------------------------------------------
Below is an example of writing your own protocol and using it.
MyModalViewController.h
#protocol MyModalViewControllerDelegate <NSObject>
//if you don't need to send any data
- (void)myModalDidFinishDismissing;
//if you need to send data
- (void)myModalDidFinishDismissingWithData:(YourType *)yourData
#end
#interface MyModalViewController : UIViewController
#property (weak) id <MyModalViewControllerDelegate> delegate;
//place the rest of your properties and public methods here
#end
The where ever you want to in your MyModalViewController implementation file, call your delegate method of choice. You should first make sure your delegate actually responds to the selector though.
MyModalViewController.m
if ( [self.delegate respondsToSelector:#selector(myModalDidFinishDismissing)] )
[self.delegate myModalDidFinishDismissing];
In your viewcontroller that is presenting the modal, you need to state in the header file that you conform to the protocol, you need to set the delegate of the modal as the viewcontroller, and make sure you actually implement the delegate method you intend on using.
MyPresentingViewController.h
#interface MyPresentingViewController : UIViewController <MyModalViewControllerDelegate>
MyPresentingViewController.m
myModal.delegate = self;
- (void)myModalDidFinishDismissing {
//do something
[tableView reloadData];
}
You can have a delegate variable in your modal form class. When you dismiss your modal form, you can call something like [delegate performSelector:#selector(modalFormClosed)] which calls the [tableView reloadData].
#interface ModalForm : UIViewController
#property (nonatomic, weak) id delegate;
#end
#implementation ModalForm
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
[self.delegate performSelector:#selector(modalFormClosed)];
}
#end
In your class that uses the ModalForm:
myModalForm.delegate = self;
And make sure you have the modalFormClosed method too:
- (void)modalFormClosed {
[self.tableView reloadData];
}
Or you can send a NSNotification (look into NSNotificationCenter) when your modal form disappears.
I wish I had the reference, but some years ago I saw a note from Apple saying a modal view should never dismiss itself. Instead you call a method on a presenting view controller. In this method the presenter dismisses the modal view controller. Typically I put a category on my UIViewControllers:
#interface UIViewController (Extension)
-(void)simpleDismissAnimated:(BOOL)animated;
#end
#implementation UIViewController (Extension)
-(void)simpleDismissAnimated:(BOOL)animated {
[self dismissViewControllerAnimated:animated completion:nil];
}
Either include it in all your view controller subclasses, or in your pch file.
So have your modal view controller call:
[self.presentingViewController simpleDismissAnimated:YES];
Then in your presenting view controller, override the implementation of simpleDismissAnimated: to something like:
-(void)simpleDismissAnimated:(BOOL)animated {
[_tableView reloadData];
[self dismissViewControllerAnimated:animated completion:nil;
}
That should take care of it.
Try this line:
[self.tableView reloadData];
You can try putting [tableView reloadData] in viewWillAppear method where your tableView is.
I had the same problem. ReloadData didn't work me. I solved it so that i made new method like this:
- (void)insertNewString:(NSString *)text{
if (!_yourArrayWithData) {
_yourArrayWithData = [[NSMutableArray alloc] init];
}
[_yourArrayWithData insertObject:text atIndex:0];
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:0];
[self.tableView insertRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
}
and when i want to add something to tableView, i use button with action like this:
- (IBAction)SaveNewPhrase:(id)sender {
NSString *mainText = _NewPhraseTextView.text; //I take nsstring from textView
if([mainText isEqual:#""]){
return;
}else{
NSMutableArray *contentFile = [NSArray arrayWithContentsOfFile:docPathContents()];
NSMutableArray *copyContentFile = [[NSMutableArray alloc] init];
//save array with new item
[copyContentFile addObject:mainText];
[copyContentFile addObjectsFromArray:contentFile];
[copyContentFile writeToFile:docPathContents() atomically:YES];
[self insertNewString:mainText]; //refresh tableView
[_NewPhraseTextView resignFirstResponder];
}}
I hope, that it helps.

Resources