iOS开发基础7-构造方法、layoutSubviews 与 Xib
自定义 View 进阶:构造方法、layoutSubviews 与 Xib
在 iOS 开发中,自定义 View 是构建可复用 UI 组件的核心手段。本文详细介绍自定义构造方法、layoutSubviews 布局机制、Xib 文件以及通过 Xib 自定义 View 的完整流程,并分析其底层逻辑。
一、自定义构造方法
自定义构造方法(初始化方法)可以让对象创建更简洁、更语义化,尤其在需要传入配置参数时非常有用。
完整实现
Objective-C
// NJShopView.h
@class NJShop;
@interface NJShopView : UIView
@property (nonatomic, strong) NJShop *shop;
/// 快速创建(无参数)
+ (instancetype)shopView;
/// 指定初始化方法:传入模型数据
- (instancetype)initWithShop:(NJShop *)shop NS_DESIGNATED_INITIALIZER;
/// 工厂方法:传入模型数据
+ (instancetype)shopViewWithShop:(NJShop *)shop;
@end
// NJShopView.m
@interface NJShopView ()
@property (nonatomic, strong) UIImageView *iconView;
@property (nonatomic, strong) UILabel *nameLabel;
@end
@implementation NJShopView
#pragma mark - 构造方法
+ (instancetype)shopView {
return [[self alloc] init];
}
- (instancetype)initWithShop:(NJShop *)shop {
// 调用指定初始化方法 initWithFrame:,确保子视图被创建
if (self = [super initWithFrame:CGRectZero]) {
self.shop = shop;
}
return self;
}
+ (instancetype)shopViewWithShop:(NJShop *)shop {
return [[self alloc] initWithShop:shop];
}
// UIView 的指定初始化方法,代码创建时调用
- (instancetype)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
[self setupSubviews];
}
return self;
}
// Storyboard/XIB 创建时调用
- (instancetype)initWithCoder:(NSCoder *)coder {
if (self = [super initWithCoder:coder]) {
[self setupSubviews];
}
return self;
}
#pragma mark - 私有方法
- (void)setupSubviews {
UIImageView *iconView = [[UIImageView alloc] init];
iconView.contentMode = UIViewContentModeScaleAspectFit;
[self addSubview:iconView];
self.iconView = iconView;
UILabel *nameLabel = [[UILabel alloc] init];
nameLabel.textAlignment = NSTextAlignmentCenter;
nameLabel.font = [UIFont systemFontOfSize:14];
[self addSubview:nameLabel];
self.nameLabel = nameLabel;
}
- (void)setShop:(NJShop *)shop {
_shop = shop;
self.iconView.image = [UIImage imageNamed:shop.icon];
self.nameLabel.text = shop.name;
}
@end
Swift
class ShopView: UIView {
private var iconView: UIImageView!
private var nameLabel: UILabel!
var shop: Shop? {
didSet {
iconView.image = UIImage(named: shop?.icon ?? "")
nameLabel.text = shop?.name
}
}
// 便利构造方法:传入模型
convenience init(shop: Shop) {
self.init(frame: .zero)
self.shop = shop
}
override init(frame: CGRect) {
super.init(frame: frame)
setupSubviews()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setupSubviews()
}
private func setupSubviews() {
iconView = UIImageView()
iconView.contentMode = .scaleAspectFit
addSubview(iconView)
nameLabel = UILabel()
nameLabel.textAlignment = .center
nameLabel.font = .systemFont(ofSize: 14)
addSubview(nameLabel)
}
}
构造方法规范
-
指定初始化方法(Designated Initializer):
UIView的指定初始化方法是initWithFrame:。所有自定义初始化方法最终都应调用它,确保子视图被统一创建。用NS_DESIGNATED_INITIALIZER标记可以让编译器检查调用链的正确性。 -
两个初始化入口都要重写:
initWithFrame:— 代码创建视图时调用。initWithCoder:— Storyboard/Xib 创建时调用。- 两者都调用公共的
setup方法,避免代码重复。
-
init与initWithFrame:的关系:UIView的init方法内部会自动调用initWithFrame:CGRectZero,因此只重写initWithFrame:即可覆盖init的情况。 -
先调用 super,再设置属性:初始化方法中必须先执行
self = [super init...],确保父类初始化完成后再访问自身属性。
底层逻辑
- 对象创建与初始化分离:
alloc分配内存并创建对象,init系列方法完成属性初始化。 - 指定初始化方法链:每个类的指定初始化方法必须调用父类的指定初始化方法,形成完整的初始化链。便利构造方法(如
shopViewWithShop:)则调用本类的指定初始化方法。 - 统一初始化逻辑:将子视图创建等公共逻辑抽取到
setupSubviews中,无论从哪个入口创建对象都能正确初始化。
二、layoutSubviews
layoutSubviews 是 UIView 的核心布局方法,用于在其中手动计算并设置子视图的 frame。
示例
Objective-C
- (void)layoutSubviews {
[super layoutSubviews];
// 注意:布局内部子视图时使用 bounds(视图自身内部坐标系),而非 frame
CGFloat width = self.bounds.size.width;
CGFloat height = self.bounds.size.height;
// 图片在上,正方形区域
CGFloat iconSize = width;
self.iconView.frame = CGRectMake(0, 0, iconSize, iconSize);
// 文字在下,占剩余高度
self.nameLabel.frame = CGRectMake(0, iconSize, width, height - iconSize);
}
Swift
override func layoutSubviews() {
super.layoutSubviews()
let width = bounds.width
let height = bounds.height
let iconSize = width
iconView.frame = CGRect(x: 0, y: 0, width: iconSize, height: iconSize)
nameLabel.frame = CGRect(x: 0, y: iconSize, width: width, height: height - iconSize)
}
bounds是视图自身的内部坐标系(原点为自身左上角),frame是相对于父视图的位置和尺寸。在layoutSubviews中布局子视图时应使用bounds.size,因为子视图是相对于自身内部排列的。
调用时机
| 场景 | 是否调用 |
|---|---|
| 视图首次被添加到父视图并需要显示时 | 是 |
bounds / frame 的尺寸发生变化 |
是 |
addSubview: 添加子视图 |
是(被添加的视图会触发) |
调用 setNeedsLayout 后,下一个绘制周期 |
是 |
调用 layoutIfNeeded |
是(立即触发) |
UIScrollView 滚动(bounds 变化) |
是 |
| 设备旋转导致视图尺寸变化 | 是 |
仅修改 frame.origin 或 center(位置变化,尺寸不变) |
否 |
| 设置与当前相同的尺寸 | 否 |
相关方法
| 方法 | 作用 |
|---|---|
setNeedsLayout |
标记视图为需要重新布局,在下一个 runloop 自动调用 layoutSubviews(异步) |
layoutIfNeeded |
如果有布局标记,立即调用 layoutSubviews(同步,用于动画中获取中间状态) |
layoutSubviews |
实际执行布局的方法,由系统调用,一般不手动直接调用 |
底层逻辑
- 懒布局机制:系统不会在每次属性变化时立即布局,而是将多个变化合并,在下一个绘制周期统一调用
layoutSubviews,避免重复计算。 - 性能优化:仅在尺寸真正变化时才触发,位置变化不影响子视图的相对布局,因此不调用。
- 与 Auto Layout 的关系:如果使用 Auto Layout,
layoutSubviews会在约束计算完成后调用,可在其中做基于 frame 的微调。纯手动布局(Frame-based)则完全依赖此方法。
三、Xib 文件
Xib(也写作 XIB,编译后为 .nib)是一种描述单个界面或视图的 XML 文件,支持可视化拖拽设计。
Xib 与 Storyboard 的关系
| 维度 | Xib | Storyboard |
|---|---|---|
| 管理范围 | 单个视图或单个视图控制器 | 多个界面及界面间的跳转流程 |
| 文件大小 | 小,加载快 | 大,包含整个场景流程 |
| 适用场景 | 自定义 View、独立的小界面、可复用组件 | 应用整体界面流程、页面间跳转 |
| 复用性 | 高,一个 Xib 可在多处加载 | 低,每个 Storyboard 场景绑定特定控制器 |
Xib 和 Storyboard 是互补关系,而非"前身"关系。Storyboard 侧重管理多页面流程,Xib 侧重单个可复用视图的设计。两者可以同时使用。
Xib 的特点
- 可视化设计:拖拽控件、设置属性、建立连线,所见即所得。
- 轻量快速:文件小,加载速度快,适合自定义控件。
- 解耦:界面布局与业务逻辑分离,提升可维护性。
四、利用 Xib 自定义 View
使用 Xib 自定义 View 可以将视图的可视化设计封装在 Xib 文件中,代码中只需负责数据绑定和逻辑处理。
完整实现
1. 创建 Xib 文件
在 Xcode 中 New File → User Interface → View,命名为 XMGShopView.xib。
2. 配置 Xib
- 选中 Xib 中顶层的
View,在 Identity Inspector 中将Class设置为XMGShopView。 - 拖入
UIImageView和UILabel,设置好约束或 frame。 - 将控件连线到代码中的
IBOutlet。
3. 代码实现
Objective-C
// XMGShopView.h
@class NJShop;
@interface XMGShopView : UIView
@property (nonatomic, strong) NJShop *shop;
+ (instancetype)shopView;
@end
// XMGShopView.m
#import "XMGShopView.h"
#import "NJShop.h"
@interface XMGShopView ()
@property (weak, nonatomic) IBOutlet UIImageView *iconView;
@property (weak, nonatomic) IBOutlet UILabel *nameLabel;
@end
@implementation XMGShopView
+ (instancetype)shopView {
return [[[NSBundle mainBundle] loadNibNamed:@"XMGShopView" owner:nil options:nil] firstObject];
}
- (void)awakeFromNib {
[super awakeFromNib];
// Xib 加载完成、连线建立后调用,适合做额外配置
self.nameLabel.font = [UIFont systemFontOfSize:14];
}
- (void)setShop:(NJShop *)shop {
_shop = shop;
self.iconView.image = [UIImage imageNamed:shop.icon];
self.nameLabel.text = shop.name;
}
@end
Swift
class ShopView: UIView {
@IBOutlet weak var iconView: UIImageView!
@IBOutlet weak var nameLabel: UILabel!
var shop: Shop? {
didSet {
iconView.image = UIImage(named: shop?.icon ?? "")
nameLabel.text = shop?.name
}
}
static func shopView() -> ShopView {
return Bundle.main.loadNibNamed("ShopView", owner: nil)?.first as! ShopView
}
override func awakeFromNib() {
super.awakeFromNib()
nameLabel.font = .systemFont(ofSize: 14)
}
}
4. 使用
XMGShopView *shopView = [XMGShopView shopView];
shopView.frame = CGRectMake(50, 100, 100, 120);
shopView.shop = self.shops[0];
[self.view addSubview:shopView];
let shopView = ShopView.shopView()
shopView.frame = CGRect(x: 50, y: 100, width: 100, height: 120)
shopView.shop = shops[0]
view.addSubview(shopView)
Xib 加载原理
调用 loadNibNamed:owner:options: 时,系统执行以下步骤:
- 解码文件:读取编译后的
.nib文件(Xib 编译产物),反序列化为对象图。 - 创建对象:根据 Xib 中的类名创建自定义类对象(调用
initWithCoder:)。 - 设置属性:恢复在 Xib 中设置的属性(背景色、字体、frame 等)。
- 创建子控件:创建并初始化所有子视图,设置其属性。
- 建立连线:通过 KVC 将子视图关联到
IBOutlet属性,将事件关联到IBAction方法。 - 添加子视图:将所有子视图添加到父视图层级中。
- 发送
awakeFromNib:所有对象创建和连线完成后,向每个对象发送awakeFromNib消息,此时IBOutlet已可用。
使用 UINib 优化多次加载
如果同一个 Xib 需要频繁加载(如 UITableViewCell 复用),使用 UINib 可以缓存解码结果,提升性能:
Objective-C
// 注册(只执行一次)
UINib *nib = [UINib nibWithNibName:@"XMGShopView" bundle:nil];
[self.tableView registerNib:nib forCellReuseIdentifier:@"cell"];
// 加载
XMGShopView *view = [[nib instantiateWithOwner:nil options:nil] firstObject];
Swift
let nib = UINib(nibName: "ShopView", bundle: nil)
tableView.register(nib, forCellReuseIdentifier: "cell")
let view = nib.instantiate(withOwner: nil).first as! ShopView
注意事项
- IBOutlet 用 weak:Xib 连线的子视图已被父视图强引用,控制器/自定义 View 对其用
weak即可,避免重复持有。 awakeFromNib中配置:initWithCoder:中IBOutlet尚未连线(为nil),额外配置应放在awakeFromNib中。- 类名匹配:Xib 中顶层 View 的 Class 必须与代码中的类名一致,否则加载后得到的是普通
UIView,自定义属性和方法不可用。 - File's Owner vs 顶层 View:两种加载模式——将顶层 View 的类设为自定义类(本文方式),或将 File's Owner 设为自定义类并通过
owner参数加载。前者更简单,适合纯视图组件。
五、总结
| 技术点 | 核心要点 |
|---|---|
| 自定义构造方法 | 重写 initWithFrame: 和 initWithCoder: 两个入口;自定义初始化方法调用指定初始化方法;公共逻辑抽取到 setup 方法 |
| layoutSubviews | 尺寸变化时调用,位置变化不调用;用 bounds 而非 frame 计算子视图;配合 setNeedsLayout / layoutIfNeeded 使用 |
| Xib | 与 Storyboard 互补,适合单个可复用视图;编译后为 .nib;频繁加载用 UINib 缓存 |
| Xib 自定义 View | loadNibNamed: 加载;awakeFromNib 中做额外配置;IBOutlet 用 weak;Xib 中 Class 必须匹配 |
- 自定义 View 的代码创建和 Xib 创建各有优势:代码方式灵活、易于版本管理;Xib 方式可视化、开发效率高。可根据团队习惯和场景选择。
- 无论哪种方式,都应保证初始化逻辑统一、布局计算准确、数据绑定清晰。

浙公网安备 33010602011771号