Silver Challenge - connecting action-target programatically


#1

For the Silver challenge, I’ve added a UISegmentedControl to the HypnosisView view programmatically and that part is OK. Now I want to connect pressing a segment button to a method. I don’t know how to do that.
I haven’t found any example in the text so far and browsing the documentation has not helped. A document:
User Experience -> Controls -> Segmented Control Programming Guide -> Using Segmented Controls
has the following code fragment to illustrate connecting an action to a target

[code]- (void)awakeFromNib
{
[segControl setSegmentCount:3];
[[segControl cell] setTag:0 forSegment:0];
[[segControl cell] setTag:1 forSegment:1];
[[segControl cell] setTag:2 forSegment:2];
[segControl setTarget:self];
[segControl setAction:@selector(segControlClicked:)];
}

  • (IBAction)segControlClicked:(id)sender
    {
    int clickedSegment = [sender selectedSegment];
    int clickedSegmentTag = [[sender cell] tagForSegment:clickedSegment];
    //…
    }[/code]

So I tried the setTarget and setAction methods with my UISegmentedControl object, but Xcode tells me that no visible @interface declares either method. Puzzled I am.
Could someone suggest either a place in the text that would help me, something in Apple’s documentation that would help me?


#2


#3

Thanks Much. Your suggestion was exactly what I needed.

You know, I was afraid someone would want to know that. The truth is I don’t know the most sensible place to put it (except maybe on the screen with UIBuilder). So I did it in two different places.
I fumbled around in a mental fog, but eventually decided I’d do something and hopefully in the fullness of time I’d understand what the best way was. First I instantiated it in the HypnosisView initializer method. That was fine until I wanted to know the tabBarFrame value which I can get in HypnosisViewController. I hacked the designated initializer for HypnosisView to pass that as a parameter and that worked OK. But, really! So I reverted the initializer method and moved the instantiation code into HypnosisViewController in the loadView method following instantiating the HypnosisView.

OK, now my confusion is bared for all to see (the ignorance is a given since I’m working through this book). Where is the most sensible place to do this. And why?


#4


#5


#6


#7

I understood the text’s guidance of using IB for something with subviews, but since HypnosisView was already programatic I thought it would be educational to understand how to add and manage the subview programmatically. And in fact it has been pretty educational. It has raised lots of questions and made me realize that I have a tenuous grasp on many of the details of the code the text has been feeding me. Not that I’m surprised that I don’t fully understand it. And, of course, that is the purpose of a challenge.


#8


#9

Hey, Mr. G. S. Alien, your useful (and perhaps not quite yet digested) set of rules for where to do things vanished! Even if I understand it today that doesn’t mean I’ll remember it a week from now when I need to know it again. The next guy dumb enough to try to do this programmatically may need the help as badly as I did.

I’m new to this board – So is there some protocol to delete some kinds of content or… what? I don’t want to be a bad citizen through ignorance.


#10

I also added the segmented controller programmatically. First in HypnosisView.m, I implemented an action method that changes colors:

[code]// Implement action method to change colors using the segmented control.

  • (void)changeRGBcolor:(UISegmentedControl *)sender
    {
    NSArray *colorArray = [NSArray arrayWithObjects:[UIColor redColor], [UIColor greenColor], [UIColor blueColor], nil];
    [self setCircleColor:[colorArray objectAtIndex:[sender selectedSegmentIndex]]];
    }
    [/code]
    Then in HypnosisViewController.m, inside the loadView method, I added the segmented controller:

[code]- (void)loadView
{
// Create a view and set it as the view of this view controller.
CGRect frame = [[UIScreen mainScreen] bounds];
HypnosisView *v = [[HypnosisView alloc] initWithFrame:frame];
[self setView:v];

// Create an array of color name strings for the controller segments.
// Then initialize the segmented control with the color names and add it as a subview of "v".
NSArray *rgbArray = [[NSArray alloc] initWithObjects:@"Red", @"Green", @"Blue", nil];
UISegmentedControl *segmentedControl = [[UISegmentedControl alloc] initWithItems];
[v addSubview:segmentedControl];

// Set the center property of the control using the "frame" CGRect size. Then set the style of the segmented control.
[segmentedControl setCenter:CGPointMake(frame.size.width / 2, frame.size.height / 2)];
[segmentedControl setSegmentedControlStyle:UISegmentedControlStyleBar];

// Add target/action for the segmented control.
[segmentedControl addTarget:nil
                     action:@selector(changeRGBcolor:)
           forControlEvents:UIControlEventValueChanged];    

}
[/code]
One issue I have is that the segmented controller is slightly off-center on the screen:

Did I correctly set the center position property of my segmented controller? Is there a way to properly center the position of the segmented controller without having to manually specify point offsets?


#11

I’m sure these naming conventions are ugly and totally wrong, but this is how I centered the UISegmentedControl horizontally:

    // Get the frame of the parent view
    CGRect hvFrame = [hv frame]; 
    // Get the default frame of the UISegmentedControl
    CGRect scCurrentFrame = [sc frame]; 
    // To center the UISegmentedControl subtract the width of the control from the width of the parent view and divide by 2
    float scLeftMargin = (hvFrame.size.width - scCurrentFrame.size.width) / 2.0; 
    CGRect scFrame = CGRectMake(scLeftMargin, 10, scCurrentFrame.size.width, scCurrentFrame.size.height);
    [sc setFrame:scFrame];

#12

I got it working…

First, add the UISegmentedControl button to HypnosisViewController.m. I did this programmatically by overriding the view controller’s loadView method.

[code]- (void)loadView
{
// create the HypnosisView
CGRect frame = [[UIScreen mainScreen] bounds];
HypnosisView *hypnoView = [[HypnosisView alloc] initWithFrame:frame];

// create the UISegmentedControl button
NSArray *colorOptions = [[NSArray alloc] initWithObjects:@"Red", @"Green", @"Blue", nil];
UISegmentedControl *colorChange = [[UISegmentedControl alloc] initWithItems:colorOptions];
    
// position the UISegmentedControl button
CGPoint centerPoint;
centerPoint.x = frame.size.width / 2;
centerPoint.y = frame.size.height / 5 * 4;
[colorChange setCenter:centerPoint];

// make HypnosisView the main view of the view controller; make UISegmentedControl a subview of HypnosisView instance
[self setView:hypnoView];
[hypnoView addSubview:colorChange];

// determine what method to call when the UISegmentedControl button is clicked
[colorChange addTarget:hypnoView action:@selector(colorChangeButtons:) forControlEvents:UIControlEventValueChanged];

}[/code]

Next, add the following method to HypnosisView.m. This method will be called each time the UISegmentedControl button is pressed. How is it called? Refer to the last line of code in the loadView method (see above).

- (void)colorChangeButtons:(id)sender { NSArray *colorOptions = [[NSArray alloc] initWithObjects:[UIColor redColor], [UIColor greenColor], [UIColor blueColor], nil]; [self setCircleColor:[colorOptions objectAtIndex:[sender selectedSegmentIndex]]]; }

Optionally, you can declare this method in HypnosisView.h. It is my understanding that interface files are there for the programmer, not the compiler. So, they aren’t really necessary?

If you compile and run this code you will notice that I left the default color of the circles at lightGrey. This means that none of the buttons in the UISegmentedControl are depressed when the app first opens.


#13

@BrianLuby Where did you get that neat little globe for the tab bar controller? I am surprised BNR didn’t include a little globe for the mapView tab bar item, could you share where you found yours? :slight_smile:


#14

@benne90 blog.twg.ca/2010/11/retina-display-icon-set/


#15

@BryanLuby Thanks!


#16

[quote=“BryanLuby”]I also added the segmented controller programmatically. First in HypnosisView.m, I implemented an action method that changes colors:

[code]// Implement action method to change colors using the segmented control.

  • (void)changeRGBcolor:(UISegmentedControl *)sender
    {
    NSArray *colorArray = [NSArray arrayWithObjects:[UIColor redColor], [UIColor greenColor], [UIColor blueColor], nil];
    [self setCircleColor:[colorArray objectAtIndex:[sender selectedSegmentIndex]]];
    }
    [/code]
    Then in HypnosisViewController.m, inside the loadView method, I added the segmented controller:

[code]- (void)loadView
{
// Create a view and set it as the view of this view controller.
CGRect frame = [[UIScreen mainScreen] bounds];
HypnosisView *v = [[HypnosisView alloc] initWithFrame:frame];
[self setView:v];

// Create an array of color name strings for the controller segments.
// Then initialize the segmented control with the color names and add it as a subview of "v".
NSArray *rgbArray = [[NSArray alloc] initWithObjects:@"Red", @"Green", @"Blue", nil];
UISegmentedControl *segmentedControl = [[UISegmentedControl alloc] initWithItems];
[v addSubview:segmentedControl];

// Set the center property of the control using the "frame" CGRect size. Then set the style of the segmented control.
[segmentedControl setCenter:CGPointMake(frame.size.width / 2, frame.size.height / 2)];
[segmentedControl setSegmentedControlStyle:UISegmentedControlStyleBar];

// Add target/action for the segmented control.
[segmentedControl addTarget:nil
                     action:@selector(changeRGBcolor:)
           forControlEvents:UIControlEventValueChanged];    

}
[/code]
One issue I have is that the segmented controller is slightly off-center on the screen:

Did I correctly set the center position property of my segmented controller? Is there a way to properly center the position of the segmented controller without having to manually specify point offsets?[/quote]

I could not have done this without looking at your code. One thing I don’t understand. Why do you set the “addTarget” to “nil”? I changed that to the name of my HypnosisView variable – “v”.


#17

@Orangecicle Either way will work. Your way is probably better and more clear for this situation.

Setting addTarget:nil will begin with the first responder and search up the responder chain until it finds the first object that responds to the changeRGBcolor: method. In this case, it is “v”.


#18

I have a question about andre3k1 and Orangecicle’s implementation.
My understanding is that if we want to promote loose coupling in our MVC model, the segmentedControl (a subview object) should set its controller, HypnosisViewController object, as the target of the action changeRGBcolor:. An example of that was given in Silver Challenge of the Whereami application (Chapter 5).

In the previous codes, the subview object (segmentedControl) set its owning view (hypnoView) as the target instead:

Wouldn’t it be better to set HypnoViewController object as the target? So that the logic of how the view is displayed (changing circle colors) is first handled by the controller as follows:

In this approach, the receiver of the action changeRGBcolor: will be the controller instead of the view. Thus, I implemented it there.
But when I tried to do that, I obtained a compiler error: “No visible @interfance for UIView declares the selector setCircleColor:”.

This is what I have in HypnoViewController.m:

- (void)colorChangeButtons:(id)sender { NSArray *colorOptions = [[NSArray alloc] initWithObjects:[UIColor redColor], [UIColor greenColor], [UIColor blueColor], nil]; [self.view setCircleColor:[colorOptions objectAtIndex:[sender selectedSegmentIndex]]]; }
Does anyone understand why I cannot access the method setCircleColor: of HypnoView? Thanks in advance. I am so confused.


#19

Here’s my code for the silver challenge:
I didn’t want to touch HypnosisView at all… I think that’s the point of OOP!!!

HypnosisViewController.h

[code]#import <Foundation/Foundation.h>
#import “HypnosisView.h”

@interface HypnosisViewController : UIViewController
{
UISegmentedControl *colors;
HypnosisView *v;
}

-(void)chooseColor:(id)sender;
@end
[/code]

HypnosisViewController.m

[code]#import “HypnosisViewController.h”
#import “HypnosisView.h”

@implementation HypnosisViewController

  • (void)loadView
    {
    // Create a view
    CGRect frame = [[UIScreen mainScreen] bounds];
    v = [[HypnosisView alloc] initWithFrame:frame];

    // Set it as the view of this view controller
    [self setView:v];

    /**************** Silver Challenge ***********************/

    // Create an array of colors (strings)
    NSArray *colorArray = [NSArray arrayWithObjects:@“Red”,
    @“Blue”, @“Green”, nil];
    // Create segmented control ‘Colors’ and fill it with items from previous array
    colors = [[UISegmentedControl alloc] initWithItems:colorArray];
    colors.frame = CGRectMake(60, 370, 200, 30);
    colors.segmentedControlStyle = UISegmentedControlStylePlain;
    colors.momentary = YES;

    // colors UISegmentedControl is a subview of v
    [v addSubview:colors];

    // Make segmented control respond to user input
    [colors addTarget:nil action:@selector(chooseColor:) forControlEvents:UIControlEventValueChanged];

    /*********************************************************************/

    // Allow v to become first responder so that motion function will work
    [v becomeFirstResponder];

}

  • (void)chooseColor:(id)sender
    // Silver Challenge Chapter 7
    {
    // NSLog(@“test1”);
    if ([colors selectedSegmentIndex] == 0){
    [v setCircleColor: [UIColor redColor]];
    // NSLog(@“pressed”);
    }else if([colors selectedSegmentIndex] == 1){
    [v setCircleColor: [UIColor blueColor]];
    }else if ([colors selectedSegmentIndex] == 2) {
    [v setCircleColor:[UIColor greenColor]];
    }

}

  • (id)initWithNibName:(NSString *)nibName bundle:(NSBundle *)bundle
    {
    // Call the superclass’s designated initializer
    self = [super initWithNibName:nil bundle:nil];

    if (self){
    // Get the tab bar item
    UITabBarItem *tbi = [self tabBarItem];

      // Give it a label
      [tbi setTitle:@"Hypnosis"];
      
      // Create a UIImage from a file
      // This will use Hypno@2x.png on retina display devices
      UIImage *i = [UIImage imageNamed:@"Hypno.png"];
      
      // Put that image on the tab bar item
      [tbi setImage:i];
    

    }

    return self;
    }

-(void) viewDidLoad
{
// Always call the implementation of view did load
[super viewDidLoad];

NSLog(@"HypnosisViewController loaded its view");

}

@end
[/code]

Hopefully someone can help me. Like I said, I don’t want to touch HypnosisView, but I can’t make it so that my segmented control bar de-selects if the phone shakes. I’ve set it to be momentary, but that isn’t the solution I wanted. I’d like for the segment to stay highlighted as long as it is the picked color.


#20

I don’t see why not touching the HypnosisView would be the point of OOP. You’re creating a new view element, the UISegmentedControl, which has really nothing to do with the controller, but is simply a subview for HypnosisView.

If you were using a xib, you would have also placed the UISegmentedControl on it and have linked the valueChanged event handler to the File Owner which would be the controller. Then the controller would call setCircleColor on the view which in turn would call setNeedsDisplay on itself.

The big question for me is what’s the best practice for adding a target to your viewController. The modal window has a presenting-/parentViewController property, but a ‘normal’ view doesn’t seem to have a reference to its controller/file owner. Should you just create an instance variable (a delegate perhaps) to store your controller at init or does that go against the coupling of views and controllers?

After some more consideration I’m guessing the authors did mean for the UISegmentedControl to be added via the ViewController. Maybe to fool some into putting it in the initWithNibName:bundle: method with regards to the whole memory warning issue?

If possible it would be great if any clarification about this topic (and my thoughts earlier today) could be given to clear this up?