Silver Challenge Solution w/ Question


#1

Before I ask the question, I’ll post my solution code…

Here is the interface for my BaseTableViewCell class:

#import <UIKit/UIKit.h>

@interface BaseTableViewCell : UITableViewCell

// I simply moved the properties from the HomepwnerItemCell class to this base class to be inherited by subclasses
@property (weak, nonatomic) UITableView *tableView;
@property (weak, nonatomic) id controller;

// The sole method is a general method that needs to be overridden by subclasses; current implementation does nothing
- (IBAction)forwardActionWithSender:(id)sender;

Although it may be inferred, here is my implementation for my BaseTableViewCell class:

#import "BaseTableViewCell.h"

@implementation BaseTableViewCell

// standard synthesis of properties
@synthesize controller;
@synthesize tableView;

// Leaving the base implementation empty 
- (IBAction)forwardActionWithSender:(id)sender
{
    
}

I then made HomepwnerItemCell a subclass of BaseTableViewCell. The interface is as follows:

#import <UIKit/UIKit.h>
#import "BaseTableViewCell.h"

@interface HomepwnerItemCell :  BaseTableViewCell

@property (weak, nonatomic) IBOutlet UIImageView *thumbnailView;
@property (weak, nonatomic) IBOutlet UILabel *nameLabel;
@property (weak, nonatomic) IBOutlet UILabel *serialNumberLabel;
@property (weak, nonatomic) IBOutlet UILabel *valueLabel;

@end

The implementation of HomepwnerItemCell is as follows:

#import "HomepwnerItemCell.h"

@implementation HomepwnerItemCell

@synthesize thumbnailView;
@synthesize nameLabel;
@synthesize serialNumberLabel;
@synthesize valueLabel;

- (IBAction)forwardActionWithSender:(id)sender
{
    if ([sender isKindOfClass:[UIButton class]]) {
        
        SEL showImageSelector = NSSelectorFromString(@"showImage:atIndexPath:");
        
        NSIndexPath *ip = [[self tableView] indexPathForCell:self];
        
        if (ip && [[self controller] respondsToSelector:showImageSelector]) {
            
            [[self controller] performSelector:showImageSelector withObject:sender withObject:ip];
        }
    }
}
@end

This code works just as the example from the chapter. In forwardActionWithSender: I could have tagged the button and then checked for the tag of the sender before proceeding. Is this an appropriate way to forward a message to the controller? In my solution, the cell still needs to know the exact selector it is asking the controller to perform even though it doesn’t have access to the controller’s interface. My thought was that this is an umbrella method which could result in different messages being sent to the controller contingent on the “identity” (button, stepper, etc) of the sender. Semi-tangental, but is forwardActionWithSender:… an acceptable method name? Suggestions for different ones?


#2

I’m still using book 2 so this may not be applicable to the circumstances of the project, but I’ve been trolling this forum to see whats changed in the 3rd edition, and if it’s worth springing for or mostly some rehashing with minor changes.

Anyway back to the question quoted above it seems to me your method name is pretty clear and straight forward. the only thing I would possibly change is adding the action as a parameter too. You could possibly simplify your code inside with a switch statement also. I’m thinking - (IBAction)forwardAction:(NSString *)selectorName WithSender:(id)senderKeep in mind I haven’t seen your particular project and don’t know if this would add to it well or not. Cheers!

By the way if you saw or used edition 2 of the book, can you elaborate(without copyright infringement of course :astonished:) How much of the book changed and wether you personally feel the upgrade is worth it? :smiley:


#3

Although it may seem that this is better:

my UITableViewCell subclass is receiving the message via target-action. I thought target-action messages have three specific signatures they accept:

And, the action triggered by a control (UIButton, UIStepper, etc etc) would not be able to pass in a selector value even in the form of a string that I can redirect to the necessary controller.

The code provided works as a general method capable of being overridden. My concerns involved the fact that I still have to know the exact selector to send to my controller although I’m not importing the controller.h for obvious reasons. Also, I didn’t know if the method name was to similar to forwardInvocation… which is completely separate. No major issues, just curious for future development.

To answer your question, I did in fact purchase the 2nd edition and loved it. I obviously purchased the 3rd edition and I think it is well worth it however, your mileage may vary. The 3rd edition has more clear explanations and lets me think of development in a different way.


#4

In my base class, I have this:


#define ROUTE(x) [self routeAction:_cmd fromObject:x];

- (void)routeAction:(SEL)act fromObject:(id)obj
{
	[self dispatchMessage:act toObject:target fromObject:obj];
}
- (void)dispatchMessage:(SEL)msg toObject:(id)obj fromObject:(UIControl *)ctl
{
	SEL newSel = NSSelectorFromString([NSStringFromSelector(msg) stringByAppendingFormat:@"atIndexPath:"]);
	NSIndexPath *ip = [tableView indexPathForCell:self];
         [obj performSelector:newSel withObject:ctl withObject:ip];
}

In every subclass, I implement the IBActions like so:

- (IBAction)buttonTapped:(id)sender
{
      ROUTE(sender);
}

#5

Joe, why do we need the target part at all? It’s creating problems for me. XCode is offering to change target to _target, but _target is a private ivar, and I’m not even sure that’s what we need. [obj target], which I thought was the solution, ends up throwing exceptions, as it’s sending the message somewhere I’m not expecting.

So I’ve just rewritten the method like so:

- (void)dispatchMessage:(SEL)msg fromObject:(UIControl *)ctl
{
    SEL newSel = NSSelectorFromString([NSStringFromSelector(msg) stringByAppendingFormat:@"atIndexPath:"]);
    NSIndexPath *ip = [tableView indexPathForCell:self];
    [[self controller] performSelector:newSel withObject:ctl withObject:ip];
}

Thanks for any insight. I couldn’t come up with your solution on my own—was doing something similar to the OP, though I recognize yours is the more correct, generalized approach. This is a toughy for a Silver.


#6

I figured out Joe’s reply. The confusing part was that the target variable is the controller variable from the example in the book. I implemented Joe’s solution in the way outlined below.

So in your base cell class header file you need to have:

[code]@interface BaseClassCell : UITableViewCell

@property (weak, nonatomic) UITableView *tableView;
@property (weak, nonatomic) id controller;

#define ROUTE(x) [self routeAction:_cmd fromObject:x];

  • (void)routeAction:(SEL)act fromObject:(id)obj;
  • (void)dispatchMessage:(SEL)msg toObject:(id)obj fromObject:(UIControl *)ctl;

@end[/code]

Then in the implementation file:

[code]- (void)routeAction:(SEL)act fromObject:(id)obj
{
[self dispatchMessage:act toObject:controller fromObject:obj];
}

  • (void)dispatchMessage:(SEL)msg toObject:(id)obj fromObject:(UIControl *)ctl
    {
    SEL newSel = NSSelectorFromString([NSStringFromSelector(msg) stringByAppendingFormat:@“atIndexPath:”]);
    NSIndexPath *ip = [tableView indexPathForCell:self];
    [obj performSelector:newSel withObject:ctl withObject:ip];
    }[/code]

In the HomepwnerItemCell.m the showImage method now only needs:

You can also leave out the #define part and then just use [self routeAction:_cmd fromObject:sender] in the showImage method.


#7

Hi,

I have solved this silver challenge with the following code, but I am not sure…
By the way, should I split up my method just like Conway’s?
Currently it works with ‘HomepwnerItemCell’ class, but I did not checked whether it could make sense with another class yet.

BNRCellBase.h

[code]@interface BNRCellBase : UITableViewCell

@property (nonatomic, weak) UITableView *tableView;
@property (nonatomic, weak) id controller;

  • (void)actionSender:(SEL)act fromObject:(id)obj;[/code]

BNRCellBase.m

[code]@implementation BNRCellBase
@synthesize tableView, controller;

  • (void)actionSender:(SEL)act fromObject:(id)obj
    {
    SEL newSelector = NSSelectorFromString([NSStringFromSelector(act) stringByAppendingString:@“atIndexPath:”]);
    NSIndexPath *ip = [[self tableView] indexPathForCell:self];
    [[self controller] performSelector:newSelector withObject:self withObject:ip];
    }[/code]

HomepwnerItemCell.h

[code]#import “BNRCellBase.h”

@interface HomepwnerItemCell : BNRCellBase

@property (weak, nonatomic) IBOutlet UIImageView *thumbnailView;
@property (weak, nonatomic) IBOutlet UILabel *nameLabel;
@property (weak, nonatomic) IBOutlet UILabel *serialNumberLabel;
@property (weak, nonatomic) IBOutlet UILabel *valueLabel;

  • (IBAction)showImage:(id)sender;[/code]

HomepwnerItemCell.m

[code]@synthesize tableView, controller;

  • (IBAction)showImage:(id)sender
    {
    [self actionSender:_cmd fromObject:sender];
    }[/code]

#8

I’ve finally done the second one of the silver challenge - UIStepper.
It was a bit tricky because I’ve never used the feature before, so I didn’t know how it works in iOS world.
Anyway…

HomepwnerItemCell.h

[code]@property (weak, nonatomic) IBOutlet UIStepper *stepper;

  • (IBAction)valueChange:(id)sender;[/code]

HomepwnerItemCell.m

[code]@synthesize stepper;

  • (IBAction)valueChange:(id)sender
    {
    ROUTE(sender);
    }[/code]

ItemsViewController.m

    • (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath

- (void)valueChange:(id)sender atIndexPath:(NSIndexPath *)ip { BNRItem *i = [[[BNRItemStore sharedStore] allItems] objectAtIndex:[ip row]]; [i setValueInDollars:[[sender stepper] value]]; [[sender tableView] reloadData]; }

Result:


#9

My code is identical to this and whenever I press the stepper in the simulator, it throws an exception:
Homepwner[4053:f803] -[UIStepper stepper]: unrecognized selector sent to instance 0x6b8b760 .

I’ve tested my base cell and it sends through the messages fine. It seems that there is a hangup getting the messages from the stepper to the item information. Any ideas?

EDIT: Solved! All I had to do was changed the identifier from (id) to (UIStepper *) in ItemsViewController.m :

- (void)valueChange:(UIStepper *)sender atIndexPath:(NSIndexPath *)ip { BNRItem *i = [[[BNRItemStore defaultStore] allItems] objectAtIndex:[ip row]]; [i setValueInDollars:sender.value]; [self.tableView reloadData]; }


#10

Thanks Sbm112 !!

I was stuck at the same place !
Saved my day !


#11

My code is almost identical to billyshih’s and without his and Joe’s help I might not have been able to figure out what to do. After I implemented the class the same way in the forums I noticed that the base class is not generic. It forces atIndexPath: to whatever selector is passed in. So in my code I removed the hard-coded atIndexPath: and added another parameter called withParameter: that allows the user to pass the additional parameters they want appended to the _cmd selector. Another approach is to build the entire selector in the subclass and pass that instead.

At first with the original implemented code and with my modifications the image was not being shown. I tracked it down to the fact I no longer needed the controller and tableView properties in the HomepwnerItemCell class since they are inherited from its superclass, BaseCell.h.

BaseCell.h

- (void)routeAction:(SEL)act withParameter:(NSString *)param fromObject:(id)obj;
- (void)dispatchMessage:(SEL)msg withParameter:(NSString *)param toObject:(id)obj fromObject:(UIControl *)ctl;

BaseCell.m

- (void)routeAction:(SEL)act withParameter:(NSString *)param fromObject:(id)obj
{   
    [self dispatchMessage:act withParameter:param toObject:controller fromObject:obj];
}

- (void)dispatchMessage:(SEL)msg withParameter:(NSString *)param toObject:(id)obj fromObject:(UIControl *)ctl
{
    SEL newSel = NSSelectorFromString([NSStringFromSelector(msg) stringByAppendingFormat:param]);
    NSIndexPath *ip = [tableView indexPathForCell:self];
    
    if (ip) {
        if ([controller respondsToSelector:newSel]) {
            [obj performSelector:newSel withObject:ctl withObject:ip];
        }
    }
    
    
}

HomepwnerItemCell.m

- (IBAction)showImage:(id)sender 
{
    [self routeAction:_cmd withParameter:@"atIndexPath:" fromObject:sender];    
}

#12

Nerburikun’s solution throws an exception when the thumbnail is clicked, but using ROUTE(x) solution with two new methods created no problem, thanks.


#13

HI!:.

I dont understand how do you make the “sender” have as original value the “stepper”'s value he got from:

[[cell stepper] setValue:[p valueInDollars]];

…how are you doing that?


#14

I am working on implementing the second half of this challenge with the UIStepper. My code is very similar to Nerburikun’s and I made the change Sbm112 did to make the exception go away.

However, I am running into an issue now where every click of separate UIStepper performs the action on the 0th row of the table view. Did anyone else run into that issue when working with this?


#15

Not here. Make sure that you check the range of your stepper, and I find if I get odd behavior, the first thing to do is create a clean build (Under product). That seems to clear out the cobwebs and makes code magically work again.

I also made one minor tweak to the stepper code in case I ever needed to use sender for bounds, view or something else. So it ends up looking like:

- (void)valueChange:(id)sender atIndexPath:(NSIndexPath *)ip
{
    BNRItem *i = [[[BNRItemStore sharedStore] allItems] objectAtIndex:[ip row]];
    [i setValueInDollars:[(UIStepper *)sender value]];
    [[self tableView] reloadData];
}

#16

[quote=“Nerburikun”]I’ve finally done the second one of the silver challenge - UIStepper.
It was a bit tricky because I’ve never used the feature before, so I didn’t know how it works in iOS world.
Anyway…

][/quote]

Wasn’t the uistepper based cell supposed to be a separate class??