I have a UIViewController with its UIView which contains a UIButton. I want to trigger a method in UIViewController on button click event.
Keeping reference of UIViewController doesn't seem to be a good idea like the following link says:
Get to UIViewController from UIView?
So I want to achive this using a delegate. Any hint on how to achieve this?
You can do something like this
CustomView.h
#import <UIKit/UIKit.h>
#protocol CustomViewDelegate <NSObject>
-(void)didButtonPressed;
#end
#interface CustomView : UIView
#property (assign) id<CustomViewDelegate> delegate;
#end
CustomView.m
#import "CustomView.h"
#implementation CustomView
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
// Initialization code
self.backgroundColor = [UIColor whiteColor];
//[self addSubview:titleLbl];
UIButton *button= [UIButton buttonWithType:UIButtonTypeRoundedRect];
button.frame = CGRectMake(100, 100, 100, 50);
[button addTarget:self.delegate action:#selector(didButtonPressed) forControlEvents:UIControlEventTouchUpInside];
[button setTitle:#"pressMe" forState:UIControlStateNormal];
[self addSubview:button];
}
return self;
}
in your ViewController.m
-(void)loadView
{
[super loadView];
CustomView *view = [[CustomView alloc]initWithFrame:self.view.bounds];
view.delegate = self;
[self.view addSubview:view];
}
This is what the responder chain was built for. When you add a target to your button, just supply nil for the target:
[mySpecialButton addTarget:nil
action:#selector(mySpecialButtonTapped:)
forControlEvents:UIControlEventTouchUpInside];
The nil target basically means "send mySpecialButtonTapped: to any object in the responder chain that can handle it".
Now you can handle this selector anywhere in the responder chain, which includes the button itself, its containing view, its containing view controller, the UIApplication, and finally your AppDelegate. Just place this method in the object most appropriate for your needs:
- (void)mySpecialButtonTapped:(id)sender {
NSLog("My special button was tapped!");
}
You don't need delegates or callback blocks (as in the accepted answer) if you just want to bubble a message up.
I guess that you expected something more fundamental then just pass some button action to controller.
I always follow MVC pattern in case of model/view/controller collaboration. It resolve your issue and many other. And I want to share my experience.
Separate controller from view and model: don't put all of the "business logic" into view-related classes; this makes the code very unusable. Make controller classes to host this code, but ensure that the controller classes don't make too many assumptions about the presentation.
Define callback APIs with #protocol, using #optional if not all the methods are required.
For view define protocol like <view class name>Protocol (example NewsViewProtocol). For controller define delegate like <view class name>Delegate (example NewsViewDelegate) and dataSource like <view class name>DataSource (example NewsViewDataSource). Keep all this #protocols in one separate file named <view class name>Protocol.h (example NewsViewProtocol.h)
Short example:
Contents of NewsView.h
//
// NewsView.h
#interface NewsView : UIView <NewsViewProtocol> {
#protected
NSObject* delegate_;
NSObject* dataSource_;
}
#end
Contents of NewsController.h and .m
//
// NewsController.h
#interface NewsController : UIViewController <NewsViewDataSource, NewsViewDelegate> {
}
#property (nonatomic, weak) UIView<NewsViewProtocol>* customView;
#end
#implementation NewsController
- (void)viewDidLoad {
[super viewDidLoad];
self.customView = (UIView<NewsViewProtocol>*)self.view;
[self.customView setDelegate:self];
[self.customView setDataSource:self];
}
#end
Contents of NewsViewProtocol.h
//
// NewsViewProtocol.h
#protocol NewsViewProtocol;
#protocol NewsViewDelegate<NSObject>
#optional
- (void)someAction;
- (void)newsView:(UIView<NewsViewProtocol>*)newsView didSelectItemAtIndexPath:(NSIndexPath *)indexPath;
#end
#protocol NewsViewDataSource<NSObject>
#required
- (id)newsView:(UIView<NewsViewProtocol>*)newsView itemAtIndexPath:(NSIndexPath *)indexPath;
- (NSInteger)numberOfItemsInNewsView:(UIView<NewsViewProtocol>*)newsView section:(NSInteger)section;
- (BOOL)newsView:(UIView<NewsViewProtocol>*)newsView shouldDisplaySection:(NSInteger)section;
#end
#protocol NewsViewProtocol<NSObject>
#required
//Never retain delegate instance into implementation of this method
- (void)setDelegate:(NSObject<NewsViewDelegate>*)delegate;
//Never retain delegate instance into implementation of this method
- (void)setDataSource:(NSObject<NewsViewDataSource>*)dataSource;
- (void)reload;
#end
You may consider that it is redundant. In simple view controller, YES. But if you develop very complex screen with huge amount of data then it gives you some advantages as:
Helps you to separate responsibility between view and controller.
Keeps your code clear.
Makes you code more reusable.
Life is easy in xCode.
At the very beginning be sure that your xib View (the one with your button inside it) is associated to the right ViewController class. Which can be the default ViewController class that comes with a new project or your custom one.
After this, here comes the magic trick! Separate your view into 2 panel. The goal is to see your xib and your viewController code (the .m file). Now press the control key of your keyboard and drag your UIButton to the code. Select IBAction. It will generate something you can call a "listener" in other language. Go to the core code of your View Controller and complete the method!
Easy as that! Have fun :)
You don't really need delegates for this - it is how UIButtons are intended to be used. Just control-click and drag from your button to the .m file for your UIViewController. This will create a new method. From there, you can either make a call to the method you wrote or just copy-paste what you have into the new method.
You can try this:
[yourButton addTarget:self action:#selector(yourButtonAction:) forControlEvents:UIControlEventTouchUpInside];
And in your selector specify the action
- (IBAction)yourButtonAction:(id)sender {
//Action to perform
}
To add a button programmatically, in myViewController.m
UIView *yourView = [[UIView alloc] init];
UIButton *yourButton = [[UIButton alloc] initWithFrame:CGRectMake(0,0,100,21)];
[yourButton addTarget:self action:#selector(yourMethod) forControlEvents:UIControlEventTouchDown];
[yourView addSubview:yourButton];
More info here.
Related
I have a UITableView and inside each header, I have a gesture recognizer. Inside the gesture handler, I want to make changes to something inside the headerview.
-(void) sectionHeaderButtonPressed: (UIButton *)sender{
NSLog(#"Header Button pressed");
//UIView *mySubView = [sender.view.subviews objectAtIndex:0];
NSLog(#"Class: %#",[self class]);
int count = [self.view.subviews count];
NSLog(#"Self.view.subviews: %u",count);
Class buttonClass = NSClassFromString(#"UIButton");
for (int i=0; i < count; i++){
int subViewCount = [[self.view.subviews objectAtIndex:i] subviews].count;
Class className = [[self.view.subviews objectAtIndex:i] class];
NSLog(#"SubView (%u) Class:%# SubViews: %u",i,className,subViewCount);
for (int j=0; j < subViewCount; j++){
Class className01 = [[[self.view.subviews objectAtIndex:i].subviews objectAtIndex:j ] class];
NSLog(#"----SubSubView (%u) Class:%#",j,[[[self.view.subviews objectAtIndex:i].subviews objectAtIndex:j ] class]);
NSLog(#"Comparing Class: %# to Class: %#",className01, buttonClass);
if (className01 == buttonClass){
[[[self.view.subviews objectAtIndex:i].subviews objectAtIndex:j ] setTitle:#"ABCDE" forState:UIControlStateNormal];
NSLog(#"found button class");
}
}
}
}
The above code is the gesture handler and I can find the button and every other subview, but this is a real hassle.
I seem to be confused about how one object can call another object. I wanted to pass the headerview down to the gesture handler but can't figure out how to do that.
How can I directly send or access the headerview and/or it's subviews. Looking at the class name won't do much good if I have several of the same class type.
The question is about the proper way to invoke an action in another object. Running thru all the subviews doesn't seem to be the proper way. Basing the id on the class name is an error prone way to go as you might have many of the same class.
I want to have a "slide in from the side" selection menu and have that modify the header based on what the user selects from the menu. They'll tap on the header, that'll call the gesture handler, that'll call the slide out menu, the slide out menu will call the header to make it change it's contents.
It's the last part that's confusing, how do I get the menu to change the header view contents when they're completely different objects.
The best approach for this would be to use a delegate, and let your ViewController do te heavy lifting for you. Some examples on how to create a delegate can be found here.
Create a custom view with a custom delegate protocol
The first thing you would need to do is create a subclass of the HeaderView you want to implement. As you already said yourself, your code is becoming kind of a hassle, and this would be a good opportunity to create a custom view.
In this custom view, recreate your header as you want, and hook up some labels/buttons either using interface builder, or programmatically (remember to change all weak pointers for UI elements to strong if you do this programmatically, I assumed you will use interface builder)
Your .h should look something like this:
#import <UIKit/UIKit.h>
#protocol CustomHeaderViewDelegate;
#interface CustomHeaderView : UIView
#property (nonatomic, weak) UILabel *someLabel;
#property (nonatomic, weak) UILabel *someButton;
#property (nonatomic, weak) id<CustomHeaderViewDelegate> delegate;
#end
#protocol CustomHeaderViewDelegate <NSObject>
- (void)customHeaderView:(CustomHeaderView *)customheaderView receivedTouchOnButton:(UIButton *)button;
#end
and your .m file:
#import "CustomHeaderView.h"
#implementation CustomHeaderView
- (IBAction)buttonPressed:(id)sender {
if (self.delegate) {
[self.delegate customHeaderView:self receivedTouchOnButton:sender];
}
}
#end
Make the viewcontroller conform to the delegate protocol
In your view controller you want to implement the delegate protocol from your newly created CustomHeaderView. Start by making sure your viewController actually conforms to the newly created protocol by adding <CustomHeaderViewDelegate> to the interface declaration in your viewcontroller's .m file. It should look something like:
...
#import "CustomHeaderView.h"
#interface YOURVIEWCONTROLLER () <CustomHeaderViewDelegate>
...
Now somewhere in your viewController you can add the following method:
- (void)customHeaderView:(CustomHeaderView *)customheaderView receivedTouchOnButton:(UIButton *)button {
[button setTitle:#"ABCDE" forState:UIControlStateNormal];
}
Adding your header to the tableView
In your viewForHeaderInSection code do something like:
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section {
CustomHeaderView headerView = [[CustomHeaderView alloc] init];
headerView.delegate = self;
/* do some more customisation */
return headerView;
}
Finding your button
If you want to find your button, or some other view in a section header, you can simply do the following:
CustomHeaderView *customHeaderView = (CustomHeaderView *)[self tableView:YOURTABLEVIEW viewForHeaderInSection:SOMESECTION];
customHeaderView.button;
I have a uiview named leftMenuView and on that view i have a button and i want to add an action to that button so that action method should call a view controller. Take a look what till i have done:
this is my leftMenuView.h
#import <UIKit/UIKit.h>
#class LeftMenuView;
#protocol LeftMenuViewProtocol <NSObject>
-(void)homeClicked;
#end
#interface LeftMenuView : UIView
#property (nonatomic,assign) id<LeftMenuViewProtocol> customDelegate;
-(IBAction)homeClickedAction:(id)sender;
#end
and in leftMenuView.m file
#import "LeftMenuView.h"
#implementation LeftMenuView
-(IBAction)homeClickedAction:(id)sender
{
[self.customDelegate homeClicked];
NSLog(#"Clicked Home");
}
#end
Now i am trying to call that method through the delegate
Now in homeViewController.h
#interface homeViewController : UIViewController<LeftMenuViewProtocol>
and now in my homeViewController.m i am trying to call that method but it is not called
-(void)homeClicked
{
NSLog(#"Clicked Home");
}
But the above method is not called where as in leftViewMenu.m that method is called. Hope any one helps me regarding this issue.
Or, assuming you have a reference on your leftMenuView in your homeViewController you can create your UIButton programmatically and set the target of your button with your homeViewController. For example, in the viewDidLoad of your homeViewController, you can do something like:
UIButton *myButton = [[UIButton alloc] initWithFrame:CGRectMake(0, 0, 100, 20)]; // set the frame you want
myButton.backgroundColor = [UIColor redColor]; // Do additional set up
[myButton addTarget:self action:#selector(homeClicked) forControlEvents:UIControlEventTouchUpInside]; // here the target of your button's action is your homeViewController
[self.leftMenuView addSubView:myButton]; // Add the button in your left view
I suppose that in homeViewController you have a property for the LeftMenuView you need to set the customDelegate to self. In the homeViewController viewDidLoad method:
self.leftMenuView.customDelegate = self;
I'm trying to call an instance method which is in my UIViewController from within a UIView. In my UIViewController I have something like:
-(void) test {
NSLog(#"test");
}
In my UIViewController, I create an instance of my UIView like so:
draggableView = [[DraggableView alloc]initWithFrame:CGRectMake(20, 190, 280, 280)];
In my draggableView, I then want to call the test instance method. How do I do this without creating a new instance of the UIViewController?
I have tried this, but it doesn't seem like a very elegant solution and I get an error "No visible #interface ..."
View does not have default method to access its view-controller object.
You need to pass the view-controller object into the view object yourself.
Typical way to do this is making a property.
#class ViewController;
#interface DraggableView : NSObject
#property (readwrite,nonatomic,assign) ViewController* theOwnerViewController;
#end
#implementation DraggableView
- (void)testCallingOwnerViewControllerMethod
{
[self.theOwnerViewController test];
}
#end
You need to set the theOwnerViewController after you created the DraggableView object.
- (void)loadView
{
draggableView = [[DraggableView alloc]initWithFrame:CGRectMake(20, 190, 280, 280)];
draggableView.theOwnerViewController = self;
//...extra view setup.
}
Use assign to avoid retain-cycle on the property.
Delegate pattern
You can do this by above pattern, but as you noticed, you need to know and forward-declare the owner view-controller class' name from its view (which is sub-node of the VC). Usually This is bad design choice because it's easily makes circular dependency (or backward dependency), which usually creates tight-coupling.
Instead, you can use delegate pattern to avoid circular dependency issue.
#protocol TestDelegate
- (void)test;
#end
#interface DraggableView : NSObject
#property(readwrite,nonatomic,assign) id<TestDelegate> testDelegate;
#end
#implementation DraggableView
- (void)test
{
[self.testDelegate test];
}
#end
You need to set the testDelegate after you created the DraggableView object.
#interface ViewController<TestDelegate>
#end
#implementation
- (void)test
{
// do something.
}
- (void)loadView
{
draggableView = [[DraggableView alloc]initWithFrame:CGRectMake(20, 190, 280, 280)];
draggableView.testDelegate = self;
//...extra view setup.
}
#end
In this case, you don't have to know the class name of the view object before you create. Any class which conforms TestDelegate protocol can be used, and now the view and VC are loosely-coupled via the protocol.
What is the viewDidLoad for UIView?
I have a UIView with xib. I would like to hide one of it's subviews when it is loaded.
I tried to use this.
- (id)initWithCoder:(NSCoder *)aDecoder{
....
_theView.hidden = YES;
}
But the subview _theView is nil at this point.
This answer didn't help me, becouse at moment of creating the UIViewController, the UIView is not created yet. It is created programaticly, later on.
Try
-awakeFromNib method
Or in xib set the view property hidden for your subview
AwakeFromNib is called only if the view loaded from nib file.
layoutSubviews is called for all views, you can add bool _loaded = yes; in the layoutSubviews function and know if the view loaded.
The accepted answer is misleading.
awakeFromNib will always be called, not just if a nib is used.
From the apple docs:
awakeFromNib:
Prepares the receiver for service after it has been loaded from an
Interface Builder archive, or nib file.
Link
In the next example I've used only a storyBoard
You can test this very easily.
This is our ViewController:
ViewController.m:
#import "ViewController.h"
#interface ViewController ()
#end
#implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(#"viewDidLoad");
}
-(void)awakeFromNib
{
NSLog(#"awakeFromNib in view controller");
}
#end
RedView.m:
#import "RedView.h"
#implementation RedView
-(void)awakeFromNib
{
NSLog(#"awakeFromNib inside RedView");
self.green.hidden = YES;
}
#end
Order of print:
awakeFromNib in view controller
awakeFromNib inside RedView
viewDidLoad
And of course the green view will be hidden.
Edit:
awakeFromNib won't be called if you use only code to create your view but you can call it yourself or better yet, create your own method.
Example without a StoryBoard (only code):
RedView.m:
#import "RedView.h"
#implementation RedView
-(void)loadRedView
{
NSLog(#"loadRedView");
self.green = [[UIView alloc]initWithFrame:CGRectMake(0, 0, 100, 100)];
self.green.backgroundColor = [UIColor greenColor];
[self addSubview:self.green];
self.green.hidden = YES;
}
#end
ViewController.m:
#import "ViewController.h"
#interface ViewController ()
#end
#implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.red = [[RedView alloc]initWithFrame:CGRectMake(0, 0, 200, 200)];
self.red.backgroundColor = [UIColor redColor];
[self.view addSubview:self.red];
[self.red loadRedView];
}
#end
There is no such method in general. The question is, where is your _theView coming from.
If your view, including its subview, is loaded from the same nib/xib/storyboard then you can use awakeFromNib which will be called after the complete object hierarchy has been loaded from the archive, so your _theView should be set as well.
If your view is created programmatically but does not create the subview for _theView itself, that means there has to be a place in your code where you add that subview. In that case you have two options
Either hide _theView from the caller after you added it
Or declare a prepareForDisplay method (or similar) on your view class and call that after your view has been created and _theView has been assigned. In that prepareForDisplay (or whatever name you choose) method you can do whatever you like, e.g. hide _theView.
I would not recommend to abuse layoutSubviews for this as it is meant for a different purpose and will be called several times during the lifetime of a view, not just once as you want it to be. Yes you can save whether it was called before, but I would consider that a hack as well. Better create your own method to initialize the view in a way you want after you set it up correctly and call that.
layoutSubviews will be call for all the views you can set you view as hidden there instead of awakeFromNib.
If you are using xib then you can set the default hidden property.
private var layoutSubviewsCounter = 0
override func layoutSubviews() {
super.layoutSubviews()
if layoutSubviewsCounter == 0 {
layoutSubviewsCounter += 1
viewDidLoad()
}
}
func viewDidLoad() {
// your code here
}
I have a UIView .xib file. This is opened from the storyboard entry point UIViewController1 as a subview. The subview has a IBButton which when pressed opens a UIViewController2. Is this possible by any chance?
First, create a segue going from your first view controller to your second. I'm going to name it OpenViewController2. We'll be able to call that segue programmatically.
In your UIView .h file, create a protocol that defines a delegate for that view:
SomethingView.h:
#protocol SomethingViewDelegate <NSObject> {
- (void)importantThingHappened;
}
...
#interface SomethingView {
id<SomethingViewDelegate> delegate;
}
#property (nonatomic, strong) id<SomethingViewDelegate> delegate;
SomethingView.m:
#implementation
#synthesize delegate;
...
// The IBAction for the button in your view
- (IBAction)buttonClicked:(id)sender {
[delegate importantThingHappened];
}
MyViewController.m, where you create your view:
// Create view
UIView *myView = ...
myView.delegate = self;
Later in MyViewController:
// Implement the protocol. This function will be called when the button action
// inside of the UIView you created is pressed
- (void)importantThingHappened {
[self performSegueWithIdentifier:#"OpenViewController2" sender:self];
}
First give your IBButton an unique tag, and in your UIViewController's viewDidLoad,
// add following line into viewDidLoad
[(UIButton*)[self.view viewWithTag:MY_UNIQUE_TAG_FOR_BUTTON] addTarget:self action:#selector(buttonPressed:) forControlEvent:UIControlEventTouchUpInside];
and finally implement the buttonPressed: for whatever you want
-(void) buttonPressed:(UIButton*)aButton {
// do what you want to do.
}