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)
    }
}

构造方法规范

  1. 指定初始化方法(Designated Initializer):UIView 的指定初始化方法是 initWithFrame:。所有自定义初始化方法最终都应调用它,确保子视图被统一创建。用 NS_DESIGNATED_INITIALIZER 标记可以让编译器检查调用链的正确性。

  2. 两个初始化入口都要重写:

    • initWithFrame: — 代码创建视图时调用。
    • initWithCoder: — Storyboard/Xib 创建时调用。
    • 两者都调用公共的 setup 方法,避免代码重复。
  3. init 与 initWithFrame: 的关系:UIView 的 init 方法内部会自动调用 initWithFrame:CGRectZero,因此只重写 initWithFrame: 即可覆盖 init 的情况。

  4. 先调用 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: 时,系统执行以下步骤:

  1. 解码文件:读取编译后的 .nib 文件(Xib 编译产物),反序列化为对象图。
  2. 创建对象:根据 Xib 中的类名创建自定义类对象(调用 initWithCoder:)。
  3. 设置属性:恢复在 Xib 中设置的属性(背景色、字体、frame 等)。
  4. 创建子控件:创建并初始化所有子视图,设置其属性。
  5. 建立连线:通过 KVC 将子视图关联到 IBOutlet 属性,将事件关联到 IBAction 方法。
  6. 添加子视图:将所有子视图添加到父视图层级中。
  7. 发送 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

注意事项

  1. IBOutlet 用 weak:Xib 连线的子视图已被父视图强引用,控制器/自定义 View 对其用 weak 即可,避免重复持有。
  2. awakeFromNib 中配置:initWithCoder: 中 IBOutlet 尚未连线(为 nil),额外配置应放在 awakeFromNib 中。
  3. 类名匹配:Xib 中顶层 View 的 Class 必须与代码中的类名一致,否则加载后得到的是普通 UIView,自定义属性和方法不可用。
  4. 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 方式可视化、开发效率高。可根据团队习惯和场景选择。
  • 无论哪种方式,都应保证初始化逻辑统一、布局计算准确、数据绑定清晰。
posted @ 2015-07-15 14:04  Mr.陳  阅读(801)  评论(0)    收藏  举报