what is the safest way to redefine the target-acti

2019-06-10 15:14发布

问题:

I’m reorganising old code by creating a custom UIView to separate UI elements that belong in the View from those that belong in the ViewController. Several buttons previously included in several ViewControllers are now in a single custom UIView and I need them to send messages to methods inside the current viewController so users can navigate to other viewControllers.

This answer on SO suggests using a delegate while this answer to the same question suggests not using a delegate. This answer to a different question also makes sense. But I can’t tell which one is best for my needs. I'm reluctant to do anything that might break the current appDelegate which manages a MultiviewViewController (which in turn switches between ViewControllers).

So my question is:

what is the safest way to redefine the target-action method so UIButtons relay messages to methods in my UIViewController ?

A ‘skinny’ version of my code below shows what I am trying to do

safe - a clarification

By safe, I simply meant reliable i.e. not likely to introduce unintended consequences at runtime. The underlying motivation for the question is to keep UI elements where they belong - i.e. in a UIView or in a UIViewController - without breaking an existing app.

ViewController.h

#import <UIKit/UIKit.h>

@interface ViewController : UIViewController

@end

ViewController.m

#import "ViewController.h"
#import "CustomView.h"

@interface ViewController ()

@end


@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    CGRect rect                 = [UIScreen mainScreen].bounds;
    float  statusBarHeight      = [[UIApplication sharedApplication] statusBarFrame].size.height;
    CGRect screenFrame          = CGRectMake(0, statusBarHeight, rect.size.width, rect.size.height - statusBarHeight);
    self.view                   = [[UIView alloc] initWithFrame: screenFrame];

    self.view.backgroundColor   = [UIColor lightGrayColor];   

    CustomView *cv              = [[CustomView alloc]initWithFrame:screenFrame];    //create an instance of custom view
    [self.view addSubview:cv];                                                      // add to your main view
}


- (void)goTo1                                             {
        NSLog(@"switch to Family 1");

    //  * commented out from the original app
    //  MultiviewAppDelegate *appDelegate = [[UIApplication sharedApplication] delegate];
    //    [parent setSelectedZone:1];
    //  [appDelegate displayView:1];
}

- (void)goTo2                                             {
        NSLog(@"switch to Family 2");

    //  * commented out from the original app
    //  MultiviewAppDelegate *appDelegate = [[UIApplication sharedApplication] delegate];
    //    [parent setSelectedZone:2];
    //  [appDelegate displayView:1];
}

- (void)goTo3                                             {
        NSLog(@"switch to Family 3");

    //  * commented out from the original app
    //  MultiviewAppDelegate *appDelegate = [[UIApplication sharedApplication] delegate];
    //    [parent setSelectedZone:3];
    //  [appDelegate displayView:1];
}

@end

CustomView.h

#import <UIKit/UIKit.h>
#import <QuartzCore/QuartzCore.h>

@interface CustomView : UIView {
}

@end

CustomView.m

#import <Foundation/Foundation.h>
#import "CustomView.h"

@interface CustomView ()

- (UIViewController *)viewController;

@end


@implementation CustomView : UIView

- (UIViewController *)viewController {
    if ([self.nextResponder isKindOfClass:UIViewController.class])
        return (UIViewController *)self.nextResponder;
    else
        return nil;
}


- (id)initWithFrame:(CGRect)frame
{
    self                                  = [super initWithFrame:[UIScreen mainScreen].bounds];
    if (self) {

        [self threeButtons];
    }
    return self;
}


- (void)buttonPicked:(UIButton*)button                          {
    NSLog(@"Button %ld : - send message to UIViewController instead.”, (long int)[button tag]);

    switch (button.tag) {
        case 1:
            //            [self goTo1];
            break;
        case 2:
            //            [self goTo2];
            break;
        case 3:
            //            [self goTo3];
            break;
        default:
            break;
    }
}



- (void)threeButtons {

    int     count                         = 3;
    int     space                         = 5;
    float   size                          = 60;

    for (int i = 1; i <= count; i++) {

        CGFloat x                         = (i * (size + space)) + 40;
        CGFloat y                         = 100;
        CGFloat wide                      = size;
        CGFloat high                      = size;
        UIButton *buttonInView            = [[UIButton alloc] initWithFrame:CGRectMake(x, y, wide, high)];

        [buttonInView setTag:i];
        [buttonInView addTarget:self action:@selector(buttonPicked:) forControlEvents:UIControlEventTouchUpInside];

        buttonInView.layer.borderWidth    = 0.25f;
        buttonInView.layer.cornerRadius   = size/2;
        [buttonInView setTitle:[NSString stringWithFormat:@"%i", i] forState:UIControlStateNormal];
        [buttonInView setTitleColor: [UIColor blackColor] forState:UIControlStateNormal];
        buttonInView.layer.borderColor    = [UIColor blackColor].CGColor;
        buttonInView.backgroundColor      = UIColor.whiteColor;

        [self addSubview:buttonInView];
    }
}

@end

回答1:

The start of the 3rd answer you linked states:

I thinks it is not a good idea to know inside the view about parent structures. It is breaks encapsulation and leads to hard maintenance, bugs and extra relations.

I think this is only half right. I would state that not only should the custom view not know anything about whatever controller might use it, no controller using the custom view should know anything about the details of the custom view.

Look at UITableView as an example. The table view knows nothing about whatever class is using the table view. This is made true through the use of its delegate and data source protocols. At the same time, no controller using the table view has any direct knowledge or hooks into the table view's view structure (beyond the specific APIs associated with table view cells).

No user of your custom view should know what UI components make up the view. It should only expose events indicating that some high level event happened, not that a specific UIButton (or whatever) was tapped.

With these thoughts in mind, the 1st answer you linked does it best. Design your custom view with an appropriate interface than hides its internal details. This can be done using delegate protocols, notifications (as in NSNotificationCenter), or event block properties.

By taking this approach, your custom view's implementation can completely change without its event interface needing to change. You could now replace a UIButton (for example) with some other custom view. You simply adjust your code to call the same delegate method (or post some appropriate notification). There's no worry that every client of the custom view now needs to be changed from calling addTarget... to some other appropriate code. All the clients just keep implementing the same old delegate method of the custom view.

In your specific case, don't think of your custom view as having three buttons that some controller needs to handle. Think of your custom view as having 3 different events that can happen. Clients of the custom view only need to know that one of the events happened and which one. But none of that info sent to the client of the custom view should include anything about a UIButton. Classify the 3 events at a more abstract level specific to what those events represent, not how they are implemented.



回答2:

The delegate protocol approach appears to solve my problem. To do this it was necessary to move the action method from the CustomView class into the ViewController class where its role is to navigate to other viewcontrollers. Initially there was a warning associated with the line

    cv.delegate                 = self;

Assigning to 'id<CustomViewDelegate>' from incompatible type 'ViewController *const __strong’.

This link describes a similar warning solved by casting the ViewController instance as the delegate. Here, it involved changing the offending statement to

    cv.delegate                 = (id <CustomViewDelegate>)self;

I'm not sure where this solution sits with the following statement from rmaddy's helpful answer

No user of your custom view should know what UI components make up the view. It should only expose events indicating that some high level event happened, not that a specific UIButton (or whatever) was tapped.

but already I can see how it would speed up maintenance of an existing app in future. I'll go with it unless someone offers something better. Thank you rmaddy.

SOLUTION

CustomView.h

#import <UIKit/UIKit.h>

@protocol CustomViewDelegate <NSObject>

-(void)buttonPressed:(UIButton*)button;

@end


@interface CustomView : UIView

@property (assign) id<CustomViewDelegate> delegate;

@end

CustomView.m

#import "CustomView.h"

@implementation CustomView 

- (id)initWithFrame:(CGRect)frame
{
    self                            = [super initWithFrame:[UIScreen mainScreen].bounds];
    if (self) {

        self.backgroundColor        = [UIColor lightGrayColor];           
        [self threeButtons];
    }
    return self;
}


- (void)threeButtons {

    int     buttonCount                 = 3;
    int     space                       = 5;
    float   buttonSize                  = 60;

    int     initialOffset               = 60;

    CGFloat horizontallyCentred         = ([UIScreen mainScreen].bounds.size.width - buttonSize) / 2;

    for (int i = 1; i <= buttonCount; i++) {

        CGFloat x                       = horizontallyCentred;
        CGFloat y                       = initialOffset + i * (buttonSize + space);
        CGFloat wide                    = buttonSize;
        CGFloat high                    = buttonSize;
        UIButton *buttonInView          = [[UIButton alloc] initWithFrame:CGRectMake(x, y, wide, high)];

        [buttonInView setTag:i];
        [buttonInView addTarget:self.delegate action:@selector(buttonPressed:) forControlEvents:UIControlEventTouchUpInside];

        buttonInView.layer.borderWidth  = 0.25f;
        buttonInView.layer.cornerRadius = buttonSize/2;
        [buttonInView setTitle:[NSString stringWithFormat:@"%i", i] forState:UIControlStateNormal];
        [buttonInView setTitleColor: [UIColor blackColor] forState:UIControlStateNormal];
        buttonInView.layer.borderColor  = [UIColor blackColor].CGColor;
        buttonInView.backgroundColor    = UIColor.whiteColor;

        [self addSubview:buttonInView];
    }
} 
@end

ViewController.h

#import <UIKit/UIKit.h>

@interface ViewController : UIViewController

@end

ViewController.m

#import "ViewController.h"
#import "CustomView.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];

CGRect rect                 = [UIScreen mainScreen].bounds;
float  statusBarHeight      = [[UIApplication sharedApplication] statusBarFrame].size.height;
CGRect screenFrame          = CGRectMake(0, statusBarHeight, rect.size.width, rect.size.height - statusBarHeight);

self.view                   = [[UIView alloc] initWithFrame: screenFrame];

CustomView *cv              = [[CustomView alloc]initWithFrame:screenFrame];    //create an instance of custom view
//    cv.delegate                 = self;
cv.delegate                 = (id <CustomViewDelegate>)self;
[self.view addSubview:cv];                                                                            }

- (void)buttonPressed:(UIButton*)button                          {
NSLog(@"Button %ld : - message sent from UIView.", (long int)[button tag]);

switch (button.tag) {
    case 1:
        [self goTo1];
        break;
    case 2:
        [self goTo2];
        break;
    case 3:
        [self goTo3];
        break;
    default:
        break;
    }
}

- (void)goTo1                                             {
    NSLog(@"switched to Family 1");
    //  MultiviewAppDelegate *appDelegate = [[UIApplication sharedApplication] delegate];
    //    [parent setSelectedZone:1];
    //  [appDelegate displayView:1];
}

 - (void)goTo2                                             {
    NSLog(@"switched to Family 2");
    //  MultiviewAppDelegate *appDelegate = [[UIApplication sharedApplication] delegate];
    //    [parent setSelectedZone:2];
    //  [appDelegate displayView:1];
}

- (void)goTo3                                             {
    NSLog(@"switched to Family 3");
    //  MultiviewAppDelegate *appDelegate = [[UIApplication sharedApplication] delegate];
    //    [parent setSelectedZone:3];
    //  [appDelegate displayView:1];
}
@end