I have a UITableView comprised of custom UITableViewCells. In each cell, there is a UILabel and a UISlider. Does anyone know how to, upon a change in value of one of the sliders, send the new value of the slider from the custom UITableViewCell (in a separate file) to the UITableViewController, so that I can then update the array from which the table was populated?
The closest I've got so far is a failed hack: firing a setSelected event when a slider value is changed. Whilst this highlights the changed custom cell, the event is not picked up by didSelectRowAtIndexPath in the UITableViewController.
Whilst code is always appreciated, a conceptual/method solution is what I am looking for.
Thank you in advance,
Jamie
What you need is called Delegate Pattern.
Quoting from there to explain what does it mean:
Delegation is a simple and powerful pattern in which one object in a
program acts on behalf of, or in coordination with, another object.
The delegating object keeps a reference to the other object—the
delegate—and at the appropriate time sends a message to it. The
message informs the delegate of an event that the delegating object is
about to handle or has just handled. The delegate may respond to the
message by updating the appearance or state of itself or other objects
in the application, and in some cases it can return a value that
affects how an impending event is handled. The main value of
delegation is that it allows you to easily customize the behavior of
several objects in one central object.
These diagrams will help you understand what goes on:
Architecture:
Operation:
Now as to how to implement it, this is what you have to do.
For Objective-C:
First of all, create delegate methods of your UITableViewCell. Lets name it ContactTableViewCell.
In your ContactTableViewCell.h file, do this:
#protocol ContactCellDelegate <NSObject>
#required
-(void) didMoveSliderWithValue:(float) value;
#end
#interface ContactTableViewCell : UITableViewCell
#property (weak, nonatomic) id<ContactCellDelegate> delegate;
Now conform your TableViewController to this delegate. Let's name your VC MyTableViewController.
In MyTableViewController.h, Do this:
#interface MyTableViewController : UIViewController <ContactCellDelegate> //Use UITableViewController if you are using that instead of UIViewController.
In your cellForRowAtIndexPath, before returning cell, add this line:
cell.delegate = self;
Add implementation for the delegate method INSIDE your MyTableViewController.m.
-(void) didMoveSliderWithValue: (float) value
{
NSLog(#"Value is : %f",value);
//Do whatever you need to do with the value after receiving it in your VC
}
Now let's get back to your ContactTableViewCell.m. In that file you must have added some IBAction to capture the value change event in slider. Let's say it is the following:
- (IBAction)sliderValueChanged:(UISlider *)sender {
self.myTextLabel.text = [#((int)sender.value) stringValue]; //Do whatever you need to do in cell.
//Now call delegate method which will send value to your view controller:
[delegate didMoveSliderWithValue:sender.value];
}
When you call delegate method, it will run the implementation that we wrote earlier in the MyTableViewController. Do whatever you need in that method.
What happens here is that your Cell sends the message to your desired VC (Which is delegate of the Cell), that "Hey, Call the delegate method that we wrote earlier in your body. I am sending you parameters right away". Your VC takes the parameters and does whatever you wanted it to do with that info and at that time.
For Swift:
First of all, your TableViewCell.swift file, create a protocol like this:
#class_protocol protocol ContactCellDelegate {
func didMoveSliderWithValue(value: Float)
}
Now in your Cell class, create a delegate property like:
var cellDelegate: ContactCellDelegate?
In your Slider IBAction, call the delegate method like this:
self.cellDelegate?.didMoveSliderWithValue(slider.value)
In your VC do these changes:
Make it conform to the delegate:
class MyTableViewController: UIViewController, ContactCellDelegate
Add this line before returning cell in cellForRowAtIndexPath
cell.cellDelegate = self //Dont forget to make it conform to the delegate method
Add the implementation of required delegate method:
func didMoveSliderWithValue(value:float) {
//do what you want
}
I have kept the Swift part precise and summarized because It should be very easy to change the detailed Obj-C explanation to Swift implementation. However If you are confused about any of the pointers above, leave a comment.
Also see: StackOverflow answer on using Delegate pattern to pass data back
Related
I have a textView "TexV" which have a custom class "TexV_Class" inherited from UITextView and I have a viewController "VC" with custom class named "VC_Class"
Now how can I make both classes "TexV_Class" and "VC_Class" delegate and make them work together? Is it even possible that same delegate method (eg. textViewDidChange) in BOTH classes runs (leaving the sequence of running for now)
I although had made both classes delegate but only one runs (that of VC_Class having textView delegate methods run)
You can't. The delegate mechanism works by having a single callback object, if you want more than one item to react based on the delegate you can go around this in one of two ways:
1- Fire a notification on one of your delegate so that the other delegate can act accordingly
2- set a custom delegate on TexV_Class that conforms to the method of UITextView that the VC_Class wants to adopt, and have TexV_Class call this delegate from it's delegate callback.
I suggest you 3 ways to do this:
1) Use NSNotificationCenter (the pattern help 1 object communicate one-to-many objects)
2) Use multicast delegate pattern. Implementation detail, you can refer this http://blog.scottlogic.com/2012/11/19/a-multicast-delegate-pattern-for-ios-controls.html
3) Use Proxy Design pattern. (This way I choosen)
class MyTextView.h
#protocol NJCustomTextViewDelegate <NSObject>
- textViewShouldBeginEditing:
- textViewDidBeginEditing:
- textViewShouldEndEditing:
- textViewDidEndEditing:
#end
#property (nonatomic, weak) id< NJCustomTextViewDelegate >textViewDelegate;
Use this:
in MyTextView.m
self.delegate = self;
- (void)textViewShouldBeginEditing:(UITextView)textView
{
// Handle business logi
// .... Do your logic here
if ([self.textViewDelegate responseToSelector:#selector(textViewShouldBeginEditing:)])
{
[self.textViewDelegate textViewShouldBeginEditing:self];
}
}
In MyViewController.m
MyTextView textView = ....
textView.textViewDelegate = self;
So I have this BaseCell class which also has this BaseCellViewModel. Of course on top of this lives some FancyViewController with FancyViewModel. The case here is that BaseCell has UIButton on it which triggers this IBAction method - that's fine and that's cool as I can do whatever I want there, but... I have no idea how should I let know FacyViewController about the fact that some action happened on BaseCell.
I can RACObserve a property in FancViewModel as it has NSArray of those cell view models, but how to monitor actual action and notify about exact action triggered on cell?
First thing that came to my mind is the delegation or notifications, but since we have RAC in our project it would be totally stupid not to use it, right?
[Edit] What I did so far...
So, it turns out youc can use RACCommand to actually handle UI events on specific button. In that case I've added:
#property (strong, nonatomic) RACCommand *showAction;
to my BaseCellViewModel with simple implementation like:
- (RACCommand *)showAction {
return [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {
NSLog(#"TEST");
return [[RACSignal empty] logAll];
}];
}
And following this pattern I had to do something in my BaseCell which turned out to be quite simple and I ended up with adding:
- (void)configureWithViewModel:(JDLBasePostCellViewModel *)viewModel {
self.viewModel = viewModel;
self.actionButton.rac_command = self.viewModel.showAction;
}
And... It works! But...
I need to present UIActionSheet whenever this happens and this can be show only when I need the current parentViewController and since I don't have this kind of information passed anywhere I don't know what to do right now.
FancyViewModel holds a private #property (nonatomic, strong) NSMutableArray <BaseCellViewModel *> *cellViewModels;, but how can I register something on FancyViewController to actually listen for execution of RACCommand on BaseCellViewModel?
There are a few ways that the cell might communicate with the view controller. A common on is via delegation. Have the cell declare a public delegate, like:
// BaseCell.h
#protocol BaseCellDelegate;
#interface BaseCell : UITableViewCell
#property(nonatomic, weak) id<BaseCellDelegate> delegate;
// ...
#end
#protocol BaseCellDelegate <NSObject>
- (void)baseCell:(BaseCell *)cell didReceiveAction:(NSString *)actionName;
#end
When the button is pressed, work out what you'd like to tell the delegate, and tell it:
// BaseCell.m
- (IBAction)buttonWasPressed:(id)sender {
self.delegate baseCell:self didReceiveAction:#"someAction";
}
Then, in the view controller, declare that you conform to the protocol:
// FancyViewController.m
#interface FancyViewController () <BaseCellDelegate>
in cellForRowAtIndexPath, set the cell's delegate:
// dequeue, etc
cell.delegate = self;
You'll now be required to implement this in the vc:
- (void)baseCell:(BaseCell *)cell didReceiveAction:(NSString *)actionName {
// the cell got an action, but at what index path?
NSIndexPath *indexPath = [self.tableView indexPathForCell:cell];
// now we can look up our model at self.model[indexPath.row]
}
UITableViewCell are volatile, reusable components. They come and go, and your button will do as well.
How about following #danh suggestion and once control is in View Controller, formulate a RAC signal programmatically.
Since I feel rather belonging to RxSwift camp :) I cannot provide source snippet, but this answer is probably what I meant.
As you already have a RACCommand in BaseCellViewModel, you can use one of its convenience signals. For example, you can track its state using executing signal:
[baseCellViewModel.showAction.executing subscribeNext:^(NSNumber *executing) {
//do something if the command is executing
}];
Bindings with RACObserve will work as well, if you need them.
You can also get the latest value from the command's underlying signal (but in the code you posted it won't work, as you use [RACSignal empty] with doesn't send any 'next' values):
[[baseCellViewModel.command.executionSignals switchToLatest] subscribeNext:^(id x) {
//do something with the value
}];
Note that you should subscribe to this signals when you create BaseCellViewModel, not in configureWithViewModel as the latter will be called many times (resulting in many subscriptions for the same signal).
Screencast: https://www.youtube.com/watch?v=ZwehDwITEyI
This is really bizarre. The problem is to do with a label outlet sitting in a custom-designed table cell. That cell is of my CustomCell class. (actually called RA_FormCell if you watch the screencast).
CustomCell.h
#property (weak, nonatomic) IBOutlet UILabel *dayOutlet;
-(void)controller:(id<CustomCellDelegate>)controller didUpdateDay:(NSString *)theDay;
CustomCell.m
// Method is called by a view controller
// (which is itself a delegate of the CustomCell class,
// hence the identifier you see below)
-(void)controller:(id<CustomCellDelegate>)controller didUpdateDay:(NSString *)theDay;
{
NSLog(#"Method called") // confirms to me that method is called
self.dayOutlet.text = #"Goodmorning";
NSLog(#"%#", self.dayOutlet.text); // displays (null)
}
That final log does actually appear, so the method is definitely being called. I have discounted the following:
self.dayOutlet.text is not written to elsewhere by any other method in the project
dayOutlet is connected to the label in the storyboard (and the label is not connected to anything else)
The label is not hidden underneath some accidental static label on the storyboard
The cell attributes on the storyboard include its class as CustomCell
No warnings or alerts in Xcode (I have been careful to avoid any circular imports)
The problem was that the controller:didUpdateDay: message was not sent to the correct instance of the cell class.
This occurred because I had not correctly assigned this cell to be the delegate for the view controller. For anyone interested, in my screencast at 3:50, you can see that I have the following in cellForRowAtIndexPath:
RA_FormCell *cell = [tableView dequeueReusableCellWithIdentifier:self.formCellArray[indexPath.row] forIndexPath:indexPath];
self.delegate = cell
However, this means that self.delegate got continually overwritten as the table cells were generated. As a result, my controller:didUpdateDay message was sent to the bottom cell of the table, and not the top one as I required.
The solution was simple - there's no need to have this second delegate at all. Instead, when the cell delegates to the view controller, it should pass self into the message it delegates:
id<CustomCellDelegate> strongDelegate = self.delegate;
if ([strongDelegate respondsToSelector:#selector(customCell:didChangeDay1:)])
[strongDelegate customCell:self didChangeDay1:[sender value]];
Then, in the implementation of this method by the delegate, simply end it by changing the outlet directly:
-(void)customCell:(RA_FormCell *)customCell didChangeDay1:(double)value
// put logic here
customCell.dayOutlet.text = #"No problem!";
In general, there should rarely be a need for a two-way delegate structure. Keep it one way, from A to B, and just remember to have A pass self in any messages it sends to B. That way, B will know the object the message came from, and be able to communicate back to A.
Thanks to Paulw11
I'm really going crazy on this.
But let me explain to you my little project first:
I have an Custom UITableView TDStartTableView.
Also in there I have some methods implemented for rendering the table. No problem there.
Inside of one TableViewCell there is a button.
When that Button is clicked it triggers this method:
- (void)triggerPush {
[self.delegate pushNextView];
}
self.delegate is specified in the .h file of TDStartTableView like this:
#property (nonatomic, weak) id<TDStartTableViewDelegate> delegate;
Also, the reference is set in my UITableViewController:
self.tableView.delegate = self;
So essentially what I'm trying to do is: Create a custom UITableView with Buttons etc. and then listen on the events from a ViewController that is implementing that UITableView and the protocol
So because the protocol forces me to implement pushNextView this method is in my UIViewController:
- (void)pushNextView {
NSLog(#"This works");
}
To this point everything works just fine, no problem there!
But now comes the tricky part.
I create a segue from my UIViewController to a new ViewController. I connect them via a segue and name the segue appropriately. pushToSecondStep.
Now one would think, that when I change the implementation of pushNextView to this
- (void)pushNextView {
[self performSegueWithIdentifier:#"pushToSecondStep" sender:self];
}
it works. But what I get is:
'Receiver (<TDFirstStepTableViewController: 0x8dc97d0>) has no segue with identifier 'pushToSecondStep''
Please help, I'm going crazy :D
The problem was, that I overwrote a constructor of TDStartTableView.
The proper form is that you implement all constructors, so that Objective-C can instantiate them all by itself:
- (id) initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
return self;
}
Also when you implement a custom UITableView Widget you shouldn't use UITableViewController but UIViewController.
Also you don't initialize your custom UITableView yourself, Storyboard already does that for you, so if you want to set special variables for it like numberOfRows then just declare a property and then set it via a setter-method outside and call [tableView reloadData].
Also, thanks https://stackoverflow.com/users/1095089/shubhank for helping me in the chat :)
Closed. This question needs details or clarity. It is not currently accepting answers.
Want to improve this question? Add details and clarify the problem by editing this post.
Closed 9 years ago.
Improve this question
I have a custom UIView which has been loaded into a different UIViewController. I have a button in the custom UIView, which calls a method inside its own class. I want this method to refresh the UIViewController which the UIView is embeded in. Currently I have tried importing the UIViewController to the UIView initializing it and then calling a public method which i have put inside the UIViewController. Nothing that happens inside this method seems to affect it though, I have even tried changing the navigation bar title and that wont work. I know the method is getting called though as it comes up in my log.
Any help?
This is where a delegation pattern comes into action. If I have not misunderstood, you want to perform some action(refresh??) on a UIViewController based on some action in a UIView, which is in turn is a part of its own view hierarchy.
Lets say your custom view is wrapped in a class CustomView. It has a method named action that is invoked at some point. Moreover, lets assume that you have used CustomView instances to some view controllers, namely, MyViewController1, MyViewController2, etc, as a part of their view hierarchies. Now, you want to perform some action (refresh) in your VCS when action method is triggered from your CustomView instances. For this purpose, you need to declare a protocol in the header file of your CustomView and will have to register a handler of this protocol (commonly known as delegate) to an instance of the CustomView. The header file would look something like this:
//CustomView.h
//your include headers...
#class CustomView;
#protocol CustomViewDelegate <NSObject>
-(void)customViewDidPerformAction:(CustomView*)customView;
#end
#interface CustomView : UIView {
//......... your ivars
id<CustomViewDelegate> _delegate;
}
//.......your properties
#property(nonatomic,weak) id<CustomViewDelegate> delegate;
-(void)action;
//....other declarations
#end
Now, you would like customViewDidPerformAction method to be called from any class (like a view controller for "refresh" purpose) whenever action method is triggered. For that purpose, in your implementation file (CustomView.m), you need to invoke the customViewDidPerformAction method stub, if available, from inside your action method:
//CustomView.m
//.......initializer and other codes
-(void)action {
//other codes
//raise a callback notifying action has been performed
if (self.delegate && [self.delegate respondsToSelector:#selector(customViewDidPerformAction:)]) {
[self.delegate customViewDidPerformAction:self];
}
}
Now, any class that conforms to CustomViewDelegate protocol can register itself as a receiver of the callback customViewDidPerformAction:. For example, lets say, our MyViewController1 view controller class conforms to the protocol. So the header to the class would look something like this:
//MyViewController1.h
//include headers
#interface MyViewController1 : UIViewController<CustomViewDelegate>
//........your properties, methods, etc
#property(nonatomic,strong) CustomView* myCustomView;
//......
#end
Then you need to register this class as a delegate of myCustomView, after instantiating myCustomView. Say you have instantiated myCustomView in the viewDidLoad method of the VC. So the method body would be something similar to:
-(void)viewDidLoad {
////...........other codes
CustomView* cv = [[CustomView alloc] initWithFrame:<# your frame size #>];
cv.delegate = self;
self.myCustomView = cv;
[cv release];
////......other codes
}
Then you also need to create a method with the same method signature as the protocol declares, inside the implementation(.m) file of the same VC and right your "refresh" code there:
-(void)customViewDidPerformAction:(CustomView*)customView {
///write your refresh code here
}
And you are all set. MyViewController1 would execute your refresh code whenever action method is performed by CustomView.
You can follow the same mechanism (conform to CustomViewDelegate protocol and implement customViewDidPerformAction: method) from within any VC, containing a CustomView in its view hierarchy, to refresh itself whenever the action is triggered.
Hope it helps.