Container View Controllers - notify parent of action - ios

Say I have a custom container view controller (MainViewController) where I do something like this:
- (void)viewDidLoad
{
[super viewDidLoad];
HomeViewController *homeVC = [[HomeViewController alloc] initWithNibName:#"HomeViewController" bundle:nil];
[self addChildViewController:homeVC];
[self.view addSubview:homeVC.view];
}
The HomeViewController will have a button, such as "go", that when pressed will need to advance to the next view controller. So I need to notify the MainViewController of this action. What is the best way to do this?
I'm using a custom container because I need to do custom transitions between the view controllers. When "go" is pressed, some of the views on the HomeViewController will animate while the views from the new view controller are animating into place.
Obviously I could give the HomeViewController a property of type MainViewController and make calls that way, but I'm hoping that there is a cleaner way with the container view controller API.

You can either use delegate or block;
Using delegate
Create a protocol :
#protocol SomeProtocol <NSObject>
- (void)someAction;
#end
Just declare a delegate in HomeViewController.h like this:
id<SomeProtocol> delegate;
and then in MainViewController's viewDidLoad set it like this:
homeVC.delegate = self;
//some where in MainViewController implement the protocol method
-(void)someAction
{
//do something
}
then when you press the button in homeVC, just simply call:
if ([self.delegate respondsToSelector:#selector(someAction)]) {
[self.delegate someAction];
}
Using Block:
In HomeViewController.h declare a block property:
typedef void (^ActionBlock)();
#property (nonatomic, copy) ActionBlock block;
then in MainViewController ViewDidLoad:
homeVC.block = ^(){
//do something
};
then when you press the button in homeVC, just simply call:
self.block();

There's another way too...
Every view controller has a parentViewController property so using that you can do this...
In MainViewController define a method for the action you want to perform...
- (void)someMethod:(BOOL)someParam;
Then in HomeViewController you can do...
MainViewController* parent = (MainViewController*)[self parentViewController];
[parent someMethod:paramValue];
HTH :)

This is a very common pattern. The parent will be the actual instance that will handle the action, by providing a protocol and a default extension.
In Swift 3:
Parent view controller:
protocol SomeProtocol {
func foo()
}
extension ParentViewController: SomeProtocol {
func foo() {
// Parent handles it
}
}
Child view controller:
#IBAction func tapGo(_ sender: Any) {
(parent as? SomeProtocol)?.foo()
}

Related

Problems Linking Up a Switch in Xcode?

I am having trouble linking up a switch. I understand how to connect the switch as an outlet and have it have actions change the boolean state of a value, but I need the switch to perform an action in a different view controller.
Here's the situation: I have a main table view controller, called View Controller A. I have a second view controller, lets call it view controller B, that controls a menu sidebar (on a regular view controller, not a table view) triggered by a bar item. I want to be able to open up the menu, hit a switch in the sidebar, and have something change in the main table view that is controlled by view controller A.
Is there any way that I can accomplish this? I seem to have no way of accessing or changing the IBOutlets in View Controller A from B. Is there a way that I can have the action in B linked with the switch change the boolean state of a value, and have an action waiting in controller A that will respond to a change in boolean? I am not sure how to solve this problem. Help is appreciated!
You should use delegation pattern. You'll have an action waiting in controller A, but instead of responding to value changed in B the action will be triggered by B when appropriate
ViewControllerB.h
// Create delegate protocol and property
#protocol ViewControllerBDelegate <NSObject>
- (void)switchPressed:(BOOL)switchStatus;
#end
#interface ViewControllerB : NSObject
#property (nonatomic,weak) id<ViewControllerBDelegate> delegate;
#end
ViewControllerB.m
// When switch is tapped, call delegate method if it is implemented by delegate object
- (IBAction)flip: (id) sender {
UISwitch *onoff = (UISwitch *) sender;
if ([self.delegate respondsToSelector:#selector(switchPressed:)]) {
[self.delegate switchPressed:onoff.on];
}
}
ViewControllerA.h
// Conform to ViewControllerB protocol
#import ViewControllerB.h
#interface ViewControllerA : NSObject,ViewControllerBDelegate
ViewControllerA.m
// Set self (VC A) as VC B's delegate
- (void)ViewDidLoadOrSomeOtherFunction {
ViewControllerB *vcB = [[ViewControllerB alloc] init];
vcB setDelegate = self;
}
// Implement delegate method
- (void)switchPressed:(BOOL)switchStatus {
if (switchStatus) {
// Make changes on VC A
}
}

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.

How to segue to another view controller when a subview with a gesture is tapped?

I have a view controller with a subview: gestureView that is referenced by the view controller via an outlet. I have assigned a tap gesture from viewDidLoad of the parent view controller to gestureView like:
UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self.gestureView action:#selector(handleTap)];
[self.gestureView addGestureRecognizer:tapGesture];
gestureView uses a custom class where handleTap: is implemented and it works fine, I can log from gestureView's handleTap:, however how can I perform a segue from the current view controller to another view controller when gestureView is tapped.
I understand that there may be easier ways to do this, but this works best for my situation.
You can use Delegate pattern or Notification pattern.
Both will work in your case, As -handleTap() method is implemented in your subview you can create protocol in your subview class.
And when you get handleTap method you can call delegate method which should be implemented in your parent class.
In your subview class Above class declaration define protocol
#protocol SubViewClassDelegate <NSObject>
#required;
- (void) subviewDidTapped:(id) sender;
#end
Define property of delegate.
#property (nonatomic, unsafe_unretained) id<SubViewClassDelegate> delegate;
In implementation file of subview's class call delegate method.
- (void) handleTap {
if ([self.delegate respondsToSelector:#selector(subviewDidTapped:)]) {
[self.delegate subviewDidTapped:yourObject]; // in yourObject you can pass data if require.
}
}
In your parent class implemented delegate of subview class
#interface ParentViewController : UIViewController <SubViewClassDelegate>
In viewDidLoad
self.gestureView.delegate = self;
And Implement the delegate method of Subview class in Parent controller.
-(void) subviewDidTapped:(id)sender {
// Navigate using performSegueWithIdentifier:
[self performSegueWithIdentifier: #"segueIdentifier" sender: self];
}
In the handleTap, perform a segue to the other view controller you intend to display next.
[self performSegueWithIdentifier: #"YOU_SEGUE_IDENTIFIER" sender: self];
The segue that you will perform is the segue in the storyboard that is from current view controller to the next view controller. (You need to create this).
You can pass parameters on to the next view controller in the prepareForSegue:sender:, once you identify that it is the correct sender by checking the segue identifier.

Invoking the button from other view controller

How to invoke (fake event of) the button which is in a second view controller when button in first view controller is clicked?
I can fake the event for a button which is in the same view controller by doing this:
- (IBAction)first:(id)sender {
NSLog(#"this is second");
}
- (IBAction)second:(id)sender {
[self.first sendActionsForControlEvents:UIControlEventTouchUpInside]; // works
}
But if the button is in a different view controller, I'm not able to fake it:
- (IBAction)first:(id)sender {
SecondViewController *SecondView = [[ViewController alloc] init];
[SecondView.button sendActionsForControlEvents: UIControlEventTouchUpInside]; // doesnt work
}
Assuming your 2nd VC is owned by your 1st VC, then
a) Add a property like this to a class extension of your FirstViewController, for example:
#import "SecondViewController.h"
#interface FirstViewController()
// Be aware of retain cycles.
#property (nonatomic, retain) SecondViewController *secondVC;
#end
b) When you create the SecondViewController, store it in the property we just created:
// Change this if you're not using a Storyboard.
self.secondVC = [[UIStoryboard storyboardWithName:#"MainStoryboard"
bundle:nil]
instantiateViewControllerWithIdentifier:#"SecondViewController"];
c) Add a method to your SecondViewController:
- (void)clickMyButton:(id)sender {
[self.myButton sendActionsForControlEvents:UIControlEventTouchUpInside];
}
d) Make it happen from your FirstViewController:
[self.secondVC clickMyButton:self];
If my 1st assumption is not correct, you still can do steps A, B, and D. Step B changes a bit: You have to set self.secondVC with the existing instance that was created somewhere else.

Does unwinding a storyboard (exit) segue replace the need for a child delegate in a parent scene?

Does unwinding a storyboard segue in ios6 replace the need to make a source scene implement a delegate to pass data back from the child scene to the parent scene in ios5?
The way I usually do it is:
Parent Controller Header:
Call the Delegate of the child scene
#interface ParentViewController : UIViewController <ChildViewControllerDelegate>
//ok not much to show here, mainly the delegate
//properties, methods etc
#end
Parent Controller Main (body):
Prep the segue, set the delegate, create a return method from child scene
-(void) prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if ([[segue identifier] isEqualToString:#"toChildScene"])
{
UINavigationController *childViewController = segue.destinationViewController;
childViewController.delegate = self;
}
}
#pragma mark - Delegate Segue Methods
-(void) childViewControllerDidSave: (ChildViewController *) controller Notes:(NSString *)sNotes
{
someTextLabel.Text = sNotes
[self dismissModalViewControllerAnimated:YES];
}
Child Controller Header:
create the delegate, reference the parent scenes methods
#class ChildViewController;
#protocol ChildViewControllerDelegate <NSObject>
-(void) childViewControllerDidSave: (ChildViewController *) controller Notes:(NSString *)sNotes
#end
#interface ChildViewController : UIViewController
#property (weak, nonatomic) id <ChildViewControllerDelegate> delegate;
//properties, methods, etc
#end
Child Controller Main (body):
call the parent scenes method
- (IBAction)someAction:(id)sender
{
[self.delegate childViewControllerDidSave:self sNotes:someTextField.text];
}
So now the million Dollar question:
Is this process now simpler in iOS 6? Can I cut a lot of the work out using unwinding a segue / exit segue? Any example would be greatly appreciated.
Yes.
Unwind segues are an abstracted form of delegation. In iOS 6, it's simpler to use unwinds rather than delegates to pass data backwards when dismissing view controllers.
In the parent view controller, create an unwind method that returns an IBAction and takes a UIStoryboardSegue as the argument:
- (IBAction)dismissToParentViewController:(UIStoryboardSegue *)segue {
ChildViewController *childVC = segue.sourceViewController;
self.someTextLabel.Text = childVC.someTextField.text;
}
Then, in the child view controller, Control-drag from your dismiss button to the green exit icon to connect the unwind segue:

Resources