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 aUIViewController
- 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
The start of the 3rd answer you linked states:
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 callingaddTarget...
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.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 theViewController
class where its role is to navigate to other viewcontrollers. Initially there was a warning associated with the lineThis link describes a similar warning solved by casting the ViewController instance as the delegate. Here, it involved changing the offending statement to
I'm not sure where this solution sits with the following statement from rmaddy's helpful answer
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
CustomView.m
ViewController.h
ViewController.m