MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Objective-C中的UI组件自定义与复用

2023-07-192.9k 阅读

一、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 的子类。这样我们可以在子类中重写一些方法来实现自定义的绘制和行为。

  1. 重写绘制方法 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

上述代码创建了一个蓝色圆形的自定义视图,并在视图控制器中添加显示。

  1. 处理触摸事件 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 组件的自定义

(一)自定义复合视图

复合视图是由多个子视图组合而成的自定义视图。例如,我们创建一个包含一个标签和一个按钮的自定义登录组件。

  1. 创建自定义复合视图类
#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
  1. 在视图控制器中使用
#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 的简化版。

  1. 自定义容器视图类
#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
  1. 在视图控制器中使用
#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 组件的复用

(一)通过代码复用

  1. 创建可复用的组件类 我们可以将一些通用的 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
  1. 在不同视图控制器中复用 在视图控制器 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 复用

  1. 创建自定义 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
  1. 在视图控制器中复用 在视图控制器的代码中注册并使用这个自定义单元格。
#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 组件自定义与复用的最佳实践

(一)遵循设计原则

  1. 单一职责原则 每个自定义 UI 组件应该有单一明确的职责。例如,上述的 CircleView 只负责绘制圆形,LoginView 专注于实现登录相关的 UI 展示和交互。这样使得组件功能清晰,易于维护和复用。如果一个组件承担过多职责,当其中一个功能需要修改时,可能会影响到其他功能,增加维护成本。

  2. 开闭原则 自定义 UI 组件应该对扩展开放,对修改关闭。比如我们的 PageContainerView,如果后续需要添加新的页面切换动画效果,我们可以通过继承该类并在子类中重写切换页面的方法来实现,而不需要修改 PageContainerView 原有的代码。这样既满足了功能扩展的需求,又保证了原有代码的稳定性。

(二)性能优化

  1. 减少绘制开销 在自定义视图的 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
  1. 复用视图资源 在复用 UI 组件时,尤其是像表格视图单元格这样的组件,要充分利用系统提供的复用机制。例如在 UITableView 中,通过 dequeueReusableCellWithIdentifier: 方法来获取可复用的单元格,避免频繁创建和销毁单元格对象,从而提高性能。

(三)代码组织与管理

  1. 文件结构清晰 将不同的自定义 UI 组件放在不同的文件中,并且按照功能模块进行分组。例如,可以将所有与登录相关的自定义组件放在一个名为 LoginComponents 的文件夹中,将通用的提示框组件放在 CommonComponents 文件夹中。这样在项目规模较大时,方便查找和管理代码。

  2. 使用协议与代理 当自定义 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 展示和按钮点击事件的触发,而具体的登录逻辑由视图控制器来实现,使得代码结构更加清晰,组件的复用性和可维护性更高。