Objective-C中的UI组件自定义与复用
一、UI 组件自定义基础
(一)视图的基本概念
在 Objective - C 开发中,视图(View)是构建用户界面的基本元素。视图是一个矩形区域,它负责管理自己的内容绘制以及用户交互响应。所有的视图都继承自 UIView
类。例如,一个简单的 UIView
创建代码如下:
UIView *myView = [[UIView alloc] initWithFrame:CGRectMake(100, 100, 200, 200)];
myView.backgroundColor = [UIColor redColor];
[self.view addSubview:myView];
上述代码创建了一个位于坐标 (100, 100)
,宽高均为 200 的红色视图,并将其添加到当前视图控制器的主视图上。
(二)自定义视图类
为了实现自定义的 UI 组件,我们通常会创建一个继承自 UIView
的子类。这样我们可以在子类中重写一些方法来实现自定义的绘制和行为。
- 重写绘制方法
drawRect:
方法是UIView
中用于绘制视图内容的方法。当视图需要更新显示时,系统会自动调用这个方法。例如,我们创建一个自定义的圆形视图:
#import "CircleView.h"
@implementation CircleView
- (void)drawRect:(CGRect)rect {
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetFillColorWithColor(context, [UIColor blueColor].CGColor);
CGRect circleRect = CGRectMake(0, 0, self.bounds.size.width, self.bounds.size.height);
CGContextAddEllipseInRect(context, circleRect);
CGContextFillPath(context);
}
@end
在视图控制器中使用这个自定义视图:
#import "ViewController.h"
#import "CircleView.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
CircleView *circleView = [[CircleView alloc] initWithFrame:CGRectMake(150, 150, 100, 100)];
[self.view addSubview:circleView];
}
@end
上述代码创建了一个蓝色圆形的自定义视图,并在视图控制器中添加显示。
- 处理触摸事件
UIView
提供了一系列方法来处理触摸事件,如touchesBegan:withEvent:
、touchesMoved:withEvent:
和touchesEnded:withEvent:
。我们可以在自定义视图类中重写这些方法来实现自定义的触摸交互。例如,创建一个可以响应点击并改变颜色的视图:
#import "ClickableView.h"
@implementation ClickableView
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
self.backgroundColor = [UIColor greenColor];
}
@end
在视图控制器中使用这个视图:
#import "ViewController.h"
#import "ClickableView.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
ClickableView *clickableView = [[ClickableView alloc] initWithFrame:CGRectMake(100, 100, 150, 150)];
clickableView.backgroundColor = [UIColor yellowColor];
[self.view addSubview:clickableView];
}
@end
当点击这个黄色视图时,它会变成绿色。
二、复杂 UI 组件的自定义
(一)自定义复合视图
复合视图是由多个子视图组合而成的自定义视图。例如,我们创建一个包含一个标签和一个按钮的自定义登录组件。
- 创建自定义复合视图类
#import "LoginView.h"
@implementation LoginView
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
UILabel *usernameLabel = [[UILabel alloc] initWithFrame:CGRectMake(20, 20, 80, 30)];
usernameLabel.text = @"用户名:";
[self addSubview:usernameLabel];
UITextField *usernameTextField = [[UITextField alloc] initWithFrame:CGRectMake(100, 20, 150, 30)];
[self addSubview:usernameTextField];
UIButton *loginButton = [UIButton buttonWithType:UIButtonTypeSystem];
loginButton.frame = CGRectMake(100, 60, 80, 30);
[loginButton setTitle:@"登录" forState:UIControlStateNormal];
[loginButton addTarget:self action:@selector(loginAction) forControlEvents:UIControlEventTouchUpInside];
[self addSubview:loginButton];
}
return self;
}
- (void)loginAction {
NSLog(@"执行登录操作");
}
@end
- 在视图控制器中使用
#import "ViewController.h"
#import "LoginView.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
LoginView *loginView = [[LoginView alloc] initWithFrame:CGRectMake(100, 100, 300, 100)];
[self.view addSubview:loginView];
}
@end
上述代码创建了一个自定义的登录视图组件,包含用户名标签、输入框和登录按钮,并且点击登录按钮会在控制台输出执行登录操作的日志。
(二)自定义容器视图
容器视图是可以包含其他视图并管理它们的布局和生命周期的视图。例如,我们创建一个简单的分页容器视图,类似于 UIPageViewController
的简化版。
- 自定义容器视图类
#import "PageContainerView.h"
@interface PageContainerView ()
@property (nonatomic, strong) NSMutableArray<UIView *> *pageViews;
@property (nonatomic, assign) NSInteger currentPage;
@end
@implementation PageContainerView
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
self.pageViews = [NSMutableArray array];
self.currentPage = 0;
}
return self;
}
- (void)addPageView:(UIView *)pageView {
[self.pageViews addObject:pageView];
pageView.frame = self.bounds;
[self addSubview:pageView];
if (self.pageViews.count == 1) {
pageView.hidden = NO;
} else {
pageView.hidden = YES;
}
}
- (void)showNextPage {
if (self.currentPage < self.pageViews.count - 1) {
UIView *currentView = self.pageViews[self.currentPage];
currentView.hidden = YES;
self.currentPage++;
UIView *nextView = self.pageViews[self.currentPage];
nextView.hidden = NO;
}
}
- (void)showPreviousPage {
if (self.currentPage > 0) {
UIView *currentView = self.pageViews[self.currentPage];
currentView.hidden = YES;
self.currentPage--;
UIView *previousView = self.pageViews[self.currentPage];
previousView.hidden = NO;
}
}
@end
- 在视图控制器中使用
#import "ViewController.h"
#import "PageContainerView.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
PageContainerView *pageContainerView = [[PageContainerView alloc] initWithFrame:CGRectMake(50, 50, 300, 200)];
[self.view addSubview:pageContainerView];
UIView *page1 = [[UIView alloc] initWithFrame:pageContainerView.bounds];
page1.backgroundColor = [UIColor redColor];
[pageContainerView addPageView:page1];
UIView *page2 = [[UIView alloc] initWithFrame:pageContainerView.bounds];
page2.backgroundColor = [UIColor blueColor];
[pageContainerView addPageView:page2];
UIButton *nextButton = [UIButton buttonWithType:UIButtonTypeSystem];
nextButton.frame = CGRectMake(200, 260, 80, 30);
[nextButton setTitle:@"下一页" forState:UIControlStateNormal];
[nextButton addTarget:pageContainerView action:@selector(showNextPage) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:nextButton];
UIButton *previousButton = [UIButton buttonWithType:UIButtonTypeSystem];
previousButton.frame = CGRectMake(100, 260, 80, 30);
[previousButton setTitle:@"上一页" forState:UIControlStateNormal];
[previousButton addTarget:pageContainerView action:@selector(showPreviousPage) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:previousButton];
}
@end
上述代码创建了一个简单的分页容器视图,包含两个不同颜色的页面,并通过按钮实现页面切换功能。
三、UI 组件的复用
(一)通过代码复用
- 创建可复用的组件类 我们可以将一些通用的 UI 组件封装成类,以便在不同的视图控制器中复用。例如,创建一个通用的提示框视图。
#import "AlertView.h"
@interface AlertView : UIView
- (instancetype)initWithMessage:(NSString *)message;
- (void)show;
- (void)hide;
@end
@implementation AlertView
- (instancetype)initWithMessage:(NSString *)message {
CGRect screenRect = [[UIScreen mainScreen] bounds];
self = [super initWithFrame:screenRect];
if (self) {
self.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.5];
UIView *alertContentView = [[UIView alloc] initWithFrame:CGRectMake(50, screenRect.size.height / 2 - 50, screenRect.size.width - 100, 100)];
alertContentView.backgroundColor = [UIColor whiteColor];
alertContentView.layer.cornerRadius = 10;
[self addSubview:alertContentView];
UILabel *messageLabel = [[UILabel alloc] initWithFrame:CGRectMake(20, 20, alertContentView.bounds.size.width - 40, 60)];
messageLabel.text = message;
messageLabel.numberOfLines = 0;
messageLabel.textAlignment = NSTextAlignmentCenter;
[alertContentView addSubview:messageLabel];
UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(hide)];
[self addGestureRecognizer:tapGesture];
}
return self;
}
- (void)show {
UIWindow *keyWindow = [[UIApplication sharedApplication] keyWindow];
[keyWindow addSubview:self];
}
- (void)hide {
[self removeFromSuperview];
}
@end
- 在不同视图控制器中复用 在视图控制器 A 中:
#import "ViewControllerA.h"
#import "AlertView.h"
@interface ViewControllerA ()
@end
@implementation ViewControllerA
- (void)viewDidLoad {
[super viewDidLoad];
UIButton *showAlertButton = [UIButton buttonWithType:UIButtonTypeSystem];
showAlertButton.frame = CGRectMake(100, 100, 150, 30);
[showAlertButton setTitle:@"显示提示框" forState:UIControlStateNormal];
[showAlertButton addTarget:self action:@selector(showAlert) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:showAlertButton];
}
- (void)showAlert {
AlertView *alertView = [[AlertView alloc] initWithMessage:@"这是一个通用的提示框"];
[alertView show];
}
@end
在视图控制器 B 中:
#import "ViewControllerB.h"
#import "AlertView.h"
@interface ViewControllerB ()
@end
@implementation ViewControllerB
- (void)viewDidLoad {
[super viewDidLoad];
UIButton *showAnotherAlertButton = [UIButton buttonWithType:UIButtonTypeSystem];
showAnotherAlertButton.frame = CGRectMake(100, 100, 150, 30);
[showAnotherAlertButton setTitle:@"显示另一个提示框" forState:UIControlStateNormal];
[showAnotherAlertButton addTarget:self action:@selector(showAnotherAlert) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:showAnotherAlertButton];
}
- (void)showAnotherAlert {
AlertView *alertView = [[AlertView alloc] initWithMessage:@"这是另一个通用提示框内容"];
[alertView show];
}
@end
通过这种方式,我们可以在不同的视图控制器中复用 AlertView
这个组件。
(二)使用 Interface Builder 复用
- 创建自定义 XIB 文件
在 Xcode 中创建一个新的 XIB 文件,例如名为
CustomTableViewCell.xib
。在 XIB 文件中设计一个自定义的表格视图单元格,添加所需的子视图,如标签、图片视图等,并设置它们的约束。 然后创建一个对应的CustomTableViewCell
类,继承自UITableViewCell
,并将 XIB 中的子视图与类中的属性进行关联。
#import "CustomTableViewCell.h"
@interface CustomTableViewCell ()
@property (weak, nonatomic) IBOutlet UILabel *titleLabel;
@property (weak, nonatomic) IBOutlet UIImageView *iconImageView;
@end
@implementation CustomTableViewCell
- (void)awakeFromNib {
[super awakeFromNib];
// 初始化代码
}
- (void)setSelected:(BOOL)selected animated:(BOOL)animated {
[super setSelected:selected animated:animated];
// 配置选中状态
}
- (void)setTitle:(NSString *)title icon:(UIImage *)icon {
self.titleLabel.text = title;
self.iconImageView.image = icon;
}
@end
- 在视图控制器中复用 在视图控制器的代码中注册并使用这个自定义单元格。
#import "ViewController.h"
#import "CustomTableViewCell.h"
@interface ViewController () <UITableViewDataSource, UITableViewDelegate>
@property (nonatomic, strong) UITableView *tableView;
@property (nonatomic, strong) NSArray<NSString *> *titles;
@property (nonatomic, strong) NSArray<UIImage *> *icons;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.titles = @[@"标题1", @"标题2", @"标题3"];
self.icons = @[[UIImage imageNamed:@"icon1"], [UIImage imageNamed:@"icon2"], [UIImage imageNamed:@"icon3"]];
self.tableView = [[UITableView alloc] initWithFrame:self.view.bounds style:UITableViewStylePlain];
self.tableView.dataSource = self;
self.tableView.delegate = self;
[self.view addSubview:self.tableView];
UINib *nib = [UINib nibWithName:@"CustomTableViewCell" bundle:nil];
[self.tableView registerNib:nib forCellReuseIdentifier:@"CustomCell"];
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.titles.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
CustomTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"CustomCell" forIndexPath:indexPath];
[cell setTitle:self.titles[indexPath.row] icon:self.icons[indexPath.row]];
return cell;
}
@end
通过这种方式,我们利用 Interface Builder 创建的 XIB 文件实现了自定义表格视图单元格的复用。
四、UI 组件自定义与复用的最佳实践
(一)遵循设计原则
-
单一职责原则 每个自定义 UI 组件应该有单一明确的职责。例如,上述的
CircleView
只负责绘制圆形,LoginView
专注于实现登录相关的 UI 展示和交互。这样使得组件功能清晰,易于维护和复用。如果一个组件承担过多职责,当其中一个功能需要修改时,可能会影响到其他功能,增加维护成本。 -
开闭原则 自定义 UI 组件应该对扩展开放,对修改关闭。比如我们的
PageContainerView
,如果后续需要添加新的页面切换动画效果,我们可以通过继承该类并在子类中重写切换页面的方法来实现,而不需要修改PageContainerView
原有的代码。这样既满足了功能扩展的需求,又保证了原有代码的稳定性。
(二)性能优化
- 减少绘制开销
在自定义视图的
drawRect:
方法中,尽量减少复杂的绘制操作。例如,避免在每次绘制时创建大量新的图形上下文对象。如果视图内容变化不大,可以考虑使用CAShapeLayer
等图层相关技术,因为图层的渲染效率通常比在drawRect:
中直接绘制要高。例如,对于前面的CircleView
,可以改为使用CAShapeLayer
来绘制圆形:
#import "CircleView.h"
#import <QuartzCore/QuartzCore.h>
@implementation CircleView
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
CAShapeLayer *circleLayer = [CAShapeLayer layer];
circleLayer.path = [UIBezierPath bezierPathWithOvalInRect:self.bounds].CGPath;
circleLayer.fillColor = [UIColor blueColor].CGColor;
[self.layer addSublayer:circleLayer];
}
return self;
}
@end
- 复用视图资源
在复用 UI 组件时,尤其是像表格视图单元格这样的组件,要充分利用系统提供的复用机制。例如在
UITableView
中,通过dequeueReusableCellWithIdentifier:
方法来获取可复用的单元格,避免频繁创建和销毁单元格对象,从而提高性能。
(三)代码组织与管理
-
文件结构清晰 将不同的自定义 UI 组件放在不同的文件中,并且按照功能模块进行分组。例如,可以将所有与登录相关的自定义组件放在一个名为
LoginComponents
的文件夹中,将通用的提示框组件放在CommonComponents
文件夹中。这样在项目规模较大时,方便查找和管理代码。 -
使用协议与代理 当自定义 UI 组件需要与外部进行交互时,使用协议与代理模式可以使组件与外部的耦合度降低。例如,在自定义的
LoginView
中,如果希望在点击登录按钮后,视图控制器能执行一些特定的登录逻辑,可以定义一个协议:
@protocol LoginViewDelegate <NSObject>
- (void)loginViewDidClickLoginButton:(LoginView *)loginView;
@end
@interface LoginView : UIView
@property (nonatomic, weak) id<LoginViewDelegate> delegate;
// 其他代码...
@end
@implementation LoginView
- (void)loginAction {
if ([self.delegate respondsToSelector:@selector(loginViewDidClickLoginButton:)]) {
[self.delegate loginViewDidClickLoginButton:self];
}
}
@end
在视图控制器中遵循该协议并实现方法:
#import "ViewController.h"
#import "LoginView.h"
@interface ViewController () <LoginViewDelegate>
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
LoginView *loginView = [[LoginView alloc] initWithFrame:CGRectMake(100, 100, 300, 100)];
loginView.delegate = self;
[self.view addSubview:loginView];
}
- (void)loginViewDidClickLoginButton:(LoginView *)loginView {
// 执行实际的登录逻辑,如网络请求等
NSLog(@"执行实际登录逻辑");
}
@end
通过这种方式,LoginView
组件只负责 UI 展示和按钮点击事件的触发,而具体的登录逻辑由视图控制器来实现,使得代码结构更加清晰,组件的复用性和可维护性更高。