在 Swift 开发 iOS App 时,自定义控件的各类写法、适用场景和最佳实践,这是一个非常核心且实用的知识点,能帮助你写出可复用、易维护的 UI 代码。
一、自定义控件的核心分类与写法
根据自定义程度和实现方式,iOS 中自定义控件主要分为以下几类,每类都有其适用场景和标准写法:
1. 组合式自定义控件(最常用)
核心思路 :不从头绘制,而是将系统原生控件(如 UIButton、UILabel、UIImageView)组合起来,封装成一个独立的 UIView 子类,对外暴露统一的接口。适用场景:业务场景化的控件(如商品卡片、导航栏、登录项),90% 的自定义控件需求都可以用这种方式实现。
标准写法示例:
swift
import UIKit
// 1. 定义自定义控件类,继承UIView
class CustomProductCard: UIView {
// 2. 封装子控件(私有化,仅内部管理)
private let iconImageView: UIImageView = {
let iv = UIImageView()
iv.contentMode = .scaleAspectFit
iv.clipsToBounds = true
iv.layer.cornerRadius = 8
// 禁用自动布局转换,使用纯AutoLayout
iv.translatesAutoresizingMaskIntoConstraints = false
return iv
}()
private let titleLabel: UILabel = {
let label = UILabel()
label.font = UIFont.systemFont(ofSize: 16, weight: .medium)
label.textColor = .black
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
private let priceLabel: UILabel = {
let label = UILabel()
label.font = UIFont.systemFont(ofSize: 14, weight: .regular)
label.textColor = .red
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
// 3. 对外暴露配置接口(属性/方法)
var productIcon: UIImage? {
didSet {
iconImageView.image = productIcon
}
}
var productTitle: String? {
didSet {
titleLabel.text = productTitle
}
}
var productPrice: String? {
didSet {
priceLabel.text = productPrice
}
}
// 4. 重写初始化方法(保证代码复用)
override init(frame: CGRect) {
super.init(frame: frame)
setupUI() // 统一初始化UI
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setupUI() // 适配Storyboard/XIB
}
// 5. 统一布局方法
private func setupUI() {
// 设置控件背景
self.backgroundColor = .white
// 添加子控件
addSubview(iconImageView)
addSubview(titleLabel)
addSubview(priceLabel)
// AutoLayout布局
NSLayoutConstraint.activate([
// 图标:左16,上16,宽80,高80
iconImageView.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 16),
iconImageView.topAnchor.constraint(equalTo: self.topAnchor, constant: 16),
iconImageView.widthAnchor.constraint(equalToConstant: 80),
iconImageView.heightAnchor.constraint(equalToConstant: 80),
// 标题:图标右侧16,与图标顶部对齐
titleLabel.leadingAnchor.constraint(equalTo: iconImageView.trailingAnchor, constant: 16),
titleLabel.topAnchor.constraint(equalTo: iconImageView.topAnchor),
// 标题右侧最多距父视图16
titleLabel.trailingAnchor.constraint(lessThanOrEqualTo: self.trailingAnchor, constant: -16),
// 价格:标题下方8,与标题左对齐
priceLabel.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor),
priceLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 8),
// 价格底部距父视图至少16
priceLabel.bottomAnchor.constraint(lessThanOrEqualTo: self.bottomAnchor, constant: -16)
])
}
}
// 6. 使用示例
// 在ViewController中
let card = CustomProductCard(frame: CGRect(x: 0, y: 100, width: UIScreen.main.bounds.width, height: 112))
card.productIcon = UIImage(named: "product_icon")
card.productTitle = "iPhone 15 Pro"
card.productPrice = "¥7999"
view.addSubview(card)
2. 绘制式自定义控件(自定义绘制内容)
核心思路 :重写draw(_:)方法或使用 CAShapeLayer 绘制自定义图形(如折线图、进度条、自定义按钮背景)。适用场景:系统控件无法满足的图形化需求(如环形进度条、自定义形状按钮)。
标准写法示例(环形进度条):
swift
import UIKit
class CircleProgressView: UIView {
// 对外暴露的进度属性(0-1)
var progress: CGFloat = 0.0 {
didSet {
// 进度变化时重绘
progressLayer.strokeEnd = progress
}
}
// 轨道层(灰色背景)
private let trackLayer = CAShapeLayer()
// 进度层(彩色进度)
private let progressLayer = CAShapeLayer()
override init(frame: CGRect) {
super.init(frame: frame)
setupLayers()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setupLayers()
}
private func setupLayers() {
// 1. 绘制圆形路径
let center = CGPoint(x: bounds.width/2, y: bounds.height/2)
let radius = min(bounds.width, bounds.height)/2 - 5 // 留5px边距
let path = UIBezierPath(arcCenter: center,
radius: radius,
startAngle: -CGFloat.pi/2, // 从顶部开始
endAngle: CGFloat.pi*3/2,
clockwise: true)
// 2. 配置轨道层
trackLayer.path = path.cgPath
trackLayer.strokeColor = UIColor.lightGray.cgColor
trackLayer.fillColor = UIColor.clear.cgColor
trackLayer.lineWidth = 8
layer.addSublayer(trackLayer)
// 3. 配置进度层
progressLayer.path = path.cgPath
progressLayer.strokeColor = UIColor.systemBlue.cgColor
progressLayer.fillColor = UIColor.clear.cgColor
progressLayer.lineWidth = 8
progressLayer.strokeEnd = 0 // 初始进度0
layer.addSublayer(progressLayer)
}
// 4. 布局变化时更新路径
override func layoutSubviews() {
super.layoutSubviews()
setupLayers() // 重新计算路径
}
}
// 使用示例
let progressView = CircleProgressView(frame: CGRect(x: 100, y: 200, width: 100, height: 100))
progressView.progress = 0.7 // 设置70%进度
view.addSubview(progressView)
3. 继承式自定义控件(扩展系统控件)
核心思路 :继承系统控件(如 UIButton、UITextField),重写其方法或属性,扩展功能。适用场景:仅需在系统控件基础上做少量修改(如自定义按钮点击效果、文本框输入限制)。
标准写法示例(自定义按钮):
swift
import UIKit
class CustomButton: UIButton {
// 自定义属性
var cornerRadius: CGFloat = 8 {
didSet {
layer.cornerRadius = cornerRadius
}
}
var normalColor: UIColor = .systemBlue {
didSet {
setBackgroundColor(normalColor, for: .normal)
}
}
var highlightColor: UIColor = .systemBlue.withAlphaComponent(0.8) {
didSet {
setBackgroundColor(highlightColor, for: .highlighted)
}
}
// 重写初始化方法
override init(frame: CGRect) {
super.init(frame: frame)
setupButton()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setupButton()
}
private func setupButton() {
// 基础配置
layer.cornerRadius = cornerRadius
clipsToBounds = true
setTitleColor(.white, for: .normal)
titleLabel?.font = UIFont.systemFont(ofSize: 16, weight: .medium)
}
// 自定义方法:设置不同状态的背景色
private func setBackgroundColor(_ color: UIColor, for state: UIControl.State) {
UIGraphicsBeginImageContext(CGSize(width: 1, height: 1))
UIGraphicsGetCurrentContext()?.setFillColor(color.cgColor)
UIGraphicsGetCurrentContext()?.fill(CGRect(x: 0, y: 0, width: 1, height: 1))
let colorImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
setBackgroundImage(colorImage, for: state)
}
// 重写触摸方法,自定义点击效果
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesBegan(touches, with: event)
// 点击时缩小
UIView.animate(withDuration: 0.1) {
self.transform = CGAffineTransform(scaleX: 0.95, y: 0.95)
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesEnded(touches, with: event)
// 松开时恢复
UIView.animate(withDuration: 0.1) {
self.transform = .identity
}
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesCancelled(touches, with: event)
// 取消触摸时恢复
UIView.animate(withDuration: 0.1) {
self.transform = .identity
}
}
}
// 使用示例
let customBtn = CustomButton(frame: CGRect(x: 50, y: 350, width: 300, height: 50))
customBtn.setTitle("自定义按钮", for: .normal)
customBtn.cornerRadius = 25
customBtn.normalColor = .systemGreen
customBtn.highlightColor = .systemGreen.withAlphaComponent(0.8)
view.addSubview(customBtn)
4. XIB/Storyboard 式自定义控件
核心思路 :用可视化工具(XIB)设计控件布局,再关联到 Swift 类中。适用场景:布局复杂、需要可视化调整的自定义控件。
标准写法步骤:
- 创建 XIB 文件(如
CustomCard.xib),拖入 UI 控件并布局; - 创建 Swift 类(
CustomCard.swift),继承 UIView; - 关联 XIB 和 Swift 类,加载 XIB:
swift
import UIKit
class CustomCard: UIView {
// 关联XIB中的控件
@IBOutlet weak var iconImageView: UIImageView!
@IBOutlet weak var titleLabel: UILabel!
// 加载XIB的核心方法
private func loadXIB() {
// 获取XIB文件
let nib = UINib(nibName: "CustomCard", bundle: nil)
// 加载XIB中的view
if let contentView = nib.instantiate(withOwner: self).first as? UIView {
// 设置frame并添加到当前view
contentView.frame = self.bounds
contentView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
self.addSubview(contentView)
}
}
override init(frame: CGRect) {
super.init(frame: frame)
loadXIB()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
loadXIB()
}
// 对外暴露配置方法
func configure(icon: UIImage?, title: String?) {
iconImageView.image = icon
titleLabel.text = title
}
}
二、自定义控件的通用最佳实践
- 封装性 :子控件私有化(
private),对外仅暴露必要的配置接口(属性 / 方法); - 初始化统一 :所有初始化路径(
init(frame:)/init(coder:))都指向同一个 UI 初始化方法; - 布局适配 :优先使用 AutoLayout,重写
layoutSubviews()处理布局变化; - 可复用性:避免硬编码(如颜色、字体、间距),通过属性暴露配置项;
- 性能优化 :
- 绘制式控件避免在
draw(_:)中做复杂计算; - 组合式控件减少层级嵌套;
- 避免频繁刷新 UI(如用
didSet触发刷新时加判断);
- 绘制式控件避免在
- 适配 Storyboard :实现
init(coder:),支持可视化编辑。
总结
- 组合式控件是最常用的写法,适合 90% 的业务场景,核心是封装系统控件并暴露统一接口;
- 绘制式控件 用于自定义图形,核心是重写
draw(_:)或使用 CAShapeLayer,需注意布局变化时更新绘制路径; - 自定义控件的核心原则:高内聚(内部逻辑封装)、低耦合(对外接口清晰)、可复用(配置项可自定义)、易适配(支持代码 / Storyboard 两种创建方式)。
遵循以上分类和最佳实践,你可以写出结构清晰、复用性高的自定义控件,大幅提升 iOS UI 开发的效率和可维护性。