Swift 开发 iOS App 过程中写自定义控件的归纳总结

在 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 类中。适用场景:布局复杂、需要可视化调整的自定义控件。

标准写法步骤

  1. 创建 XIB 文件(如CustomCard.xib),拖入 UI 控件并布局;
  2. 创建 Swift 类(CustomCard.swift),继承 UIView;
  3. 关联 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
    }
}

二、自定义控件的通用最佳实践

  1. 封装性 :子控件私有化(private),对外仅暴露必要的配置接口(属性 / 方法);
  2. 初始化统一 :所有初始化路径(init(frame:)/init(coder:))都指向同一个 UI 初始化方法;
  3. 布局适配 :优先使用 AutoLayout,重写layoutSubviews()处理布局变化;
  4. 可复用性:避免硬编码(如颜色、字体、间距),通过属性暴露配置项;
  5. 性能优化
    • 绘制式控件避免在draw(_:)中做复杂计算;
    • 组合式控件减少层级嵌套;
    • 避免频繁刷新 UI(如用didSet触发刷新时加判断);
  6. 适配 Storyboard :实现init(coder:),支持可视化编辑。

总结

  1. 组合式控件是最常用的写法,适合 90% 的业务场景,核心是封装系统控件并暴露统一接口;
  2. 绘制式控件 用于自定义图形,核心是重写draw(_:)或使用 CAShapeLayer,需注意布局变化时更新绘制路径;
  3. 自定义控件的核心原则:高内聚(内部逻辑封装)、低耦合(对外接口清晰)、可复用(配置项可自定义)、易适配(支持代码 / Storyboard 两种创建方式)。

遵循以上分类和最佳实践,你可以写出结构清晰、复用性高的自定义控件,大幅提升 iOS UI 开发的效率和可维护性。

相关推荐
黑化旺仔9 小时前
【OC】KVO
macos·ios·objective-c·cocoa
HouWan11 小时前
SwiftUI 小技巧:如何读取 SwiftUI Font 的详细信息
ios·swiftui·apple
李游Leo12 小时前
HarmonyOS 7 实战开发 05:把页面做到可上线状态
ios·harmonyos
终端安全笔记17 小时前
描述文件、企业证书、MDM 不是三个名字,是三层
android·ios·智能手机
传奇开心果编程17 小时前
【SwiftUI入门练中学】第1课 从零开始
学习·ui·ios·swiftui·swift
丿Wayne1 天前
iOS Token 刷新实战(Actor 避免并发重复刷新)
ios·swift·token·actor·swift并发
终端安全笔记2 天前
iOS 27 给了租赁一个新工具,但它只认受监督的设备
android·网络·安全·ios
大熊猫侯佩2 天前
独立 App 首发完成 iPhone Duo 展开态适配
ios·swiftui·swift
光影少年2 天前
setImmediate 和 setTimeout(0) 的区别
android·前端·react.js·ios·前端框架
2501_916008892 天前
SSE 和 gRPC 流式接口如何抓包,调试 SSE 与 gRPC 流式接口
网络协议·计算机网络·网络安全·ios·adb·https·udp